• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alibaba / jetcache / #448

23 May 2026 01:38PM UTC coverage: 89.341% (+0.4%) from 88.921%
#448

push

areyouok
fix: init fail if Kryo not in classpath

0 of 4 new or added lines in 1 file covered. (0.0%)

27 existing lines in 9 files now uncovered.

5029 of 5629 relevant lines covered (89.34%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.15
/jetcache-core/src/main/java/com/alicp/jetcache/support/DecodeFilter.java
1
/**
2
 * Created on 2026-05-21.
3
 */
4
package com.alicp.jetcache.support;
5

6
import org.slf4j.Logger;
7
import org.slf4j.LoggerFactory;
8

9
import java.io.ObjectInputFilter;
10
import java.util.Set;
11
import java.util.concurrent.ConcurrentHashMap;
12
import java.util.concurrent.CopyOnWriteArraySet;
13

14
/**
15
 * Deserialization filter to prevent arbitrary class deserialization attacks.
16
 * <p>
17
 * By default, only classes in commonly used Java packages and the JetCache package are allowed.
18
 * Users can add additional patterns via {@link #addAllowPatterns(String...)}.
19
 * <p>
20
 * A shared default instance is available via {@link #getDefault()}. Custom instances can be
21
 * created via the public constructor if needed (e.g. for per-context isolation).
22
 * <p>
23
 * Pattern syntax (applies to both allow and deny patterns):
24
 * <ul>
25
 *   <li>Prefix (ends with "."): matches all classes in the package and subpackages, e.g. "java.util." matches
26
 *       java.util.HashMap and java.util.concurrent.ConcurrentHashMap</li>
27
 *   <li>Exact class name: matches a single class exactly, e.g. "org.myapp.dto.UserDTO" matches only that class</li>
28
 *   <li>Package-only (no trailing ".", not a class name): matches only classes directly in the package,
29
 *       e.g. "java.lang" matches java.lang.String but not java.lang.reflect.Proxy</li>
30
 * </ul>
31
 *
32
 * @author huangli
33
 */
34
public class DecodeFilter {
35

36
    private static final Logger logger = LoggerFactory.getLogger(DecodeFilter.class);
1✔
37

38
    /**
39
     * Default allow patterns: commonly used Java packages for cache values.
40
     * <p>
41
     * "java.lang" uses package-only matching so subpackages like reflect/invoke are automatically excluded.
42
     * "java.util." / "java.time." use prefix matching to include subpackages.
43
     * "java.math" uses package-only matching (no subpackages exist under java.math).
44
     * "java.net" uses package-only matching (direct classes only, excluding subpackages like spi).
45
     * <p>
46
     * java.io is NOT included by default; users can add it via configuration if needed.
47
     */
48
    public static final Set<String> DEFAULT_ALLOW_PATTERNS = Set.of(
1✔
49
            "java.lang",
50
            "java.util.",
51
            "java.time.",
52
            "java.math",
53
            "java.net",
54
            "com.alicp.jetcache."
55
    );
56

57
    /**
58
     * Default deny patterns: explicitly blocked packages, subpackages and exact class names.
59
     * <p>
60
     * Deny patterns have highest priority and cannot be overridden by allow patterns.
61
     */
62
    public static final Set<String> DEFAULT_DENY_PATTERNS = Set.of(
1✔
63
            // java.lang dangerous subpackages (defense-in-depth for "java.lang." prefix)
64
            "java.lang.reflect.",
65
            "java.lang.invoke.",
66
            "java.lang.management.",
67
            "java.lang.instrument.",
68
            "java.lang.module.",
69
            "java.lang.constant.",
70
            // java.lang dangerous classes
71
            "java.lang.Runtime",
72
            "java.lang.ProcessBuilder",
73
            "java.lang.ProcessImpl",
74
            "java.lang.UNIXProcess",
75
            "java.lang.Shutdown",
76
            "java.lang.Thread",
77
            "java.lang.ThreadGroup",
78
            "java.lang.ClassLoader",
79
            "java.lang.System",
80
            "java.lang.SecurityManager",
81
            "java.lang.StackWalker",
82
            // java.beans EventHandler (classic deserialization gadget)
83
            "java.beans.EventHandler",
84
            // JNDI/RMI (high risk gadget chains)
85
            "javax.naming.",
86
            "java.rmi.",
87
            // javax.script (ScriptEngineManager gadget)
88
            "javax.script.",
89
            // javax.management (JMX MLet/remote gadget chains, defense-in-depth: ClassLoader/RMI already blocked)
90
            "javax.management.",
91
            // JDK internal classes (com.sun/sun are JDK internal APIs, no business classes exist here)
92
            "com.sun.",
93
            "sun.",
94
            // Apache Commons Collections gadget chains (InvokerTransformer, ChainedTransformer, LazyMap etc.)
95
            "org.apache.commons.collections.functors.",
96
            "org.apache.commons.collections.map.LazyMap",
97
            "org.apache.commons.collections4.functors.",
98
            "org.apache.commons.collections4.map.LazyMap",
99
            // Apache Commons BeanUtils (BeanComparator gadget)
100
            "org.apache.commons.beanutils.",
101
            // Groovy runtime (MethodClosure, ConvertedClosure gadgets)
102
            "org.codehaus.groovy.runtime.",
103
            "org.codehaus.groovy.reflection.",
104
            // C3P0 (PoolBackedDataSource gadget)
105
            "com.mchange.v2.",
106
            // Spring expression language (SpEL injection)
107
            "org.springframework.expression.",
108
            // Spring framework gadget chains (defense-in-depth: sink classes blocked by com.sun.)
109
            "org.springframework.aop.framework.JdkDynamicAopProxy",
110
            "org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider",
111
            // AspectJ Weaver (defense-in-depth: chain also requires Commons Collections which is already blocked)
112
            "org.aspectj.weaver.",
113
            // Hibernate gadget chains (TypedValue hashCode trigger → ComponentType → TemplatesImpl/JNDI)
114
            "org.hibernate.engine.spi.TypedValue",
115
            "org.hibernate.type.ComponentType",
116
            "org.hibernate.tuple.component.AbstractComponentTuplizer",
117
            "org.hibernate.property.access.spi.GetterMethodImpl",
118
            // Hibernate 4.x getter (ysoserial uses this instead of GetterMethodImpl for 4.x)
119
            "org.hibernate.property.BasicPropertyAccessor$BasicGetter",
120
            "org.hibernate.internal.util.ValueHolder",
121
            // Hessian (gadget chains)
122
            "com.caucho.",
123
            // javassist (bytecode manipulation used in gadget chains)
124
            "javassist.",
125
            // Jython (Python script execution)
126
            "org.python.",
127
            // Mozilla Rhino (JavaScript execution)
128
            "org.mozilla.javascript.",
129
            // BeanShell (script execution)
130
            "bsh.",
131
            // Clojure (script execution)
132
            "clojure.",
133
            // ROME (ToStringBean/ObjectBean gadget chains)
134
            "com.rometools.",
135
            // Vaadin (NestedMethodProperty gadget chain)
136
            "com.vaadin.",
137
            // Apache Click (Column$ColumnComparator gadget chain)
138
            "org.apache.click.",
139
            // Apache Wicket (DiskFileItem file write gadget)
140
            "org.apache.wicket."
141
    );
142

143
    private static final DecodeFilter INSTANCE = new DecodeFilter();
1✔
144

145
    private volatile boolean enabled = true;
1✔
146
    private final CopyOnWriteArraySet<String> allowPatterns;
147
    private final CopyOnWriteArraySet<String> denyPatterns;
148
    private final ConcurrentHashMap<String, Boolean> cache = new ConcurrentHashMap<>();
1✔
149

150
    public DecodeFilter() {
151
        this(DEFAULT_ALLOW_PATTERNS, DEFAULT_DENY_PATTERNS);
1✔
152
    }
1✔
153

154
    public DecodeFilter(Set<String> allowPatterns, Set<String> denyPatterns) {
1✔
155
        this.allowPatterns = new CopyOnWriteArraySet<>(allowPatterns);
1✔
156
        this.denyPatterns = new CopyOnWriteArraySet<>(denyPatterns);
1✔
157
    }
1✔
158

159
    public static DecodeFilter getDefault() {
160
        return INSTANCE;
1✔
161
    }
162

163
    /**
164
     * Check if a class name is allowed for deserialization.
165
     * <p>
166
     * When this method returns {@code false}, an error log is automatically emitted
167
     * with the specific block reason (e.g. matched deny pattern, no matching allow pattern).
168
     *
169
     * @param className the fully qualified class name (may include array type notation)
170
     * @return true if allowed, false if blocked
171
     */
172
    public boolean isAllowed(String className) {
173
        if (!enabled) {
1✔
174
            return true;
1✔
175
        }
176
        if (className == null || className.isEmpty()) {
1✔
177
            return true;
1✔
178
        }
179

180
        // Check cache first (only positive results are cached)
181
        Boolean cached = cache.get(className);
1✔
182
        if (cached != null) {
1✔
183
            return cached;
1✔
184
        }
185

186
        String blockReason = checkBlockedReason(className);
1✔
187
        if (blockReason != null) {
1✔
188
            logBlocked(className, blockReason);
1✔
189
            return false;
1✔
190
        }
191

192
        // Only cache positive results to prevent unbounded growth from attack payloads
193
        cache.put(className, true);
1✔
194
        return true;
1✔
195
    }
196

197
    private String checkBlockedReason(String className) {
198
        String componentType = extractComponentType(className);
1✔
199
        if (componentType == null) {
1✔
200
            return null;
1✔
201
        }
202
        if (componentType.isEmpty() && className.startsWith("[")) {
1✔
203
            return "invalid array type notation";
1✔
204
        }
205

206
        String nameToCheck = componentType.isEmpty() ? className : componentType;
1✔
207

208
        // Deny list has highest priority (allow patterns cannot bypass deny patterns)
209
        // Deny uses the same matching logic as allow: prefix, exact, and package-only.
210
        // WARNING: deny patterns can be removed via removeDenyPatterns/clearDenyPatterns,
211
        // which is a high-risk operation that weakens the security baseline
212
        for (String deny : denyPatterns) {
1✔
213
            if (matches(deny, nameToCheck)) {
1✔
214
                return "matched deny pattern: '" + deny + "'";
1✔
215
            }
216
        }
1✔
217

218
        for (String pattern : allowPatterns) {
1✔
219
            if (matches(pattern, nameToCheck)) {
1✔
220
                return null;
1✔
221
            }
222
        }
1✔
223

224
        return "no matching allow pattern found";
1✔
225
    }
226

227
    private String extractComponentType(String type) {
228
        if (!type.startsWith("[")) {
1✔
229
            return "";
1✔
230
        }
231

232
        int i = 0;
1✔
233
        while (i < type.length() && type.charAt(i) == '[') {
1✔
234
            i++;
1✔
235
        }
236

237
        if (i >= type.length()) {
1✔
UNCOV
238
            return "";
×
239
        }
240

241
        char c = type.charAt(i);
1✔
242
        if (c == 'L') {
1✔
243
            int start = i + 1;
1✔
244
            int end = type.length() - 1;
1✔
245
            if (end <= start || type.charAt(type.length() - 1) != ';') {
1✔
246
                return "";
1✔
247
            }
248
            return type.substring(start, end);
1✔
249
        } else if (c == 'B' || c == 'C' || c == 'D' || c == 'F' || c == 'I' || c == 'J' || c == 'S' || c == 'Z') {
1✔
250
            return null;
1✔
251
        } else {
252
            return "";
1✔
253
        }
254
    }
255

256
    private boolean matches(String pattern, String className) {
257
        if (pattern.endsWith(".")) {
1✔
258
            // Prefix matching (recursive into subpackages)
259
            return className.startsWith(pattern);
1✔
260
        }
261
        // Exact class name matching
262
        if (className.equals(pattern)) {
1✔
263
            return true;
1✔
264
        }
265
        // Package-only matching: class is directly in this package, not subpackages
266
        String prefix = pattern + ".";
1✔
267
        return className.startsWith(prefix)
1✔
268
                && className.indexOf('.', prefix.length()) == -1;
1✔
269
    }
270

271
    /**
272
     * Enable or disable the filter check.
273
     * When toggled, the cache is cleared.
274
     *
275
     * @param enabled true to enable, false to disable
276
     */
277
    public synchronized void setEnabled(boolean enabled) {
278
        this.enabled = enabled;
1✔
279
        cache.clear();
1✔
280
    }
1✔
281

282
    /**
283
     * @return true if filter is enabled
284
     */
285
    public boolean isEnabled() {
286
        return enabled;
1✔
287
    }
288

289
    /**
290
     * Add allow patterns.
291
     * Cache is cleared once after all patterns are added.
292
     *
293
     * @param patterns patterns to add
294
     */
295
    public synchronized void addAllowPatterns(String... patterns) {
296
        for (String pattern : patterns) {
1✔
297
            if (pattern != null && !pattern.isEmpty()) {
1✔
298
                allowPatterns.add(pattern);
1✔
299
            }
300
        }
301
        cache.clear();
1✔
302
    }
1✔
303

304
    /**
305
     * Remove allow patterns.
306
     * Cache is cleared once after all patterns are removed.
307
     *
308
     * @param patterns patterns to remove
309
     */
310
    public synchronized void removeAllowPatterns(String... patterns) {
311
        boolean changed = false;
1✔
312
        for (String pattern : patterns) {
1✔
313
            if (pattern != null && allowPatterns.remove(pattern)) {
1✔
314
                changed = true;
1✔
315
            }
316
        }
317
        if (changed) {
1✔
318
            cache.clear();
1✔
319
        }
320
    }
1✔
321

322
    /**
323
     * Clear all allow patterns.
324
     * Cache is cleared after this operation.
325
     */
326
    public synchronized void clearAllowPatterns() {
327
        allowPatterns.clear();
1✔
328
        cache.clear();
1✔
329
    }
1✔
330

331
    /**
332
     * Add deny patterns.
333
     * Cache is cleared once after all patterns are added.
334
     *
335
     * @param patterns patterns to add
336
     */
337
    public synchronized void addDenyPatterns(String... patterns) {
338
        for (String pattern : patterns) {
1✔
339
            if (pattern != null && !pattern.isEmpty()) {
1✔
340
                denyPatterns.add(pattern);
1✔
341
            }
342
        }
343
        cache.clear();
1✔
344
    }
1✔
345

346
    /**
347
     * Remove deny patterns.
348
     * Cache is cleared once after all patterns are removed.
349
     * <p>
350
     * <b>WARNING:</b> Removing default deny patterns (such as {@code java.rmi.},
351
     * {@code javax.naming.}, {@code java.lang.Runtime}, etc.) weakens the security
352
     * baseline and may expose the application to deserialization attacks.
353
     * Only remove deny patterns if you fully understand the security implications.
354
     *
355
     * @param patterns patterns to remove
356
     */
357
    public synchronized void removeDenyPatterns(String... patterns) {
358
        boolean changed = false;
1✔
359
        for (String pattern : patterns) {
1✔
360
            if (pattern != null && denyPatterns.remove(pattern)) {
1✔
361
                changed = true;
1✔
362
            }
363
        }
364
        if (changed) {
1✔
365
            cache.clear();
1✔
366
        }
367
    }
1✔
368

369
    /**
370
     * Clear all deny patterns.
371
     * Cache is cleared after this operation.
372
     * <p>
373
     * <b>WARNING:</b> This removes the entire security deny list, including built-in
374
     * protections against known deserialization gadget chains (JNDI/RMI, {@code Runtime},
375
     * {@code ProcessBuilder}, etc.). Only use this if you have an alternative security
376
     * mechanism in place. Calling {@link #reset()} is safer if you want to restore defaults.
377
     */
378
    public synchronized void clearDenyPatterns() {
379
        denyPatterns.clear();
1✔
380
        cache.clear();
1✔
381
    }
1✔
382

383
    /**
384
     * Clear the internal class name cache.
385
     */
386
    public synchronized void clearCache() {
387
        cache.clear();
1✔
388
    }
1✔
389

390
    /**
391
     * Reset to default state (for testing purposes).
392
     */
393
    synchronized void reset() {
394
        enabled = true;
1✔
395
        allowPatterns.clear();
1✔
396
        allowPatterns.addAll(DEFAULT_ALLOW_PATTERNS);
1✔
397
        denyPatterns.clear();
1✔
398
        denyPatterns.addAll(DEFAULT_DENY_PATTERNS);
1✔
399
        cache.clear();
1✔
400
    }
1✔
401

402
    /**
403
     * ObjectInputFilter implementation for use with ObjectInputStream.setObjectInputFilter.
404
     * <p>
405
     * Returns {@link ObjectInputFilter.Status#REJECTED} for classes not allowed by the filter,
406
     * which causes the JDK to throw {@link java.io.InvalidClassException}.
407
     * An error log with configuration guidance is also emitted.
408
     *
409
     * @param filterInfo the filter info provided by ObjectInputStream
410
     * @return ALLOWED if class is allowed by the filter, REJECTED if blocked, UNDECIDED if filter disabled
411
     */
412
    public static ObjectInputFilter.Status javaFilter(ObjectInputFilter.FilterInfo filterInfo) {
413
        if (filterInfo.serialClass() == null) {
1✔
414
            return ObjectInputFilter.Status.UNDECIDED;
1✔
415
        }
416
        if (!getDefault().isEnabled()) {
1✔
417
            return ObjectInputFilter.Status.UNDECIDED;
1✔
418
        }
419
        String className = filterInfo.serialClass().getName();
1✔
420
        if (getDefault().isAllowed(className)) {
1✔
421
            return ObjectInputFilter.Status.ALLOWED;
1✔
422
        }
423
        return ObjectInputFilter.Status.REJECTED;
1✔
424
    }
425

426
    private static void logBlocked(String className, String reason) {
427
        logger.error("Class '{}' is not allowed by the deserialization filter and has been blocked for security.\n" +
1✔
428
                "\nReason: {}\n" +
429
                "\nTo allow this class, add a pattern to your configuration:\n" +
430
                "\nYAML:\n" +
431
                "\n  jetcache:\n" +
432
                "    decodeFilterAllowPatterns:\n" +
433
                "      - com.example.\n" +
434
                "\nOr programmatically:\n" +
435
                "\n  DecodeFilter.getDefault().addAllowPatterns(\"com.example.\");\n" +
436
                "\nIf you have configured deny patterns, this class may also be blocked by them.\n" +
437
                "Check your 'decodeFilterDenyPatterns' configuration if applicable.\n" +
438
                "\nYou can also disable the filter (NOT RECOMMENDED):\n" +
439
                "\n  jetcache.decodeFilterEnabled: false",
440
                className, reason);
441
    }
1✔
442

443
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc