For Java developers, the ability to prevent insecure deserialization in Java with filters has moved from a niche hardening step to a core security requirement. While many teams have relied on blocklists of known dangerous classes, JEP 415 introduces a more resilient, data-driven approach that stops gadget chains at the source. This article explores why traditional defenses are crumbling, how to configure object filters effectively, and how custom filter logic can keep your applications secure in 2026’s threat landscape.
Why Traditional Deserialization Defenses Fall Short
Deserialization vulnerabilities often feel like whack-a-mole. Classic mitigations—maintaining a blocklist of forbidden classes like InvokerTransformer or using network-level firewalls—are fragile. Attackers continually discover new gadget chains: sequences of classes that, when deserialized together, execute arbitrary code. A blocklist only knows yesterday’s attacks, not tomorrow’s. Worse, blocklists are brittle in large applications where third-party libraries introduce obscure classes that are hard to catalog.
Moreover, many developers mistakenly think that signing serialized data or using encrypted transport solves the problem. While these controls protect against tampering and eavesdropping, they do nothing for deserialization attacks from legitimate, authenticated users—the classic “trusted input” blind spot. In modern microservice architectures, serialized objects flow through queues, caches, and RPC layers, expanding the attack surface beyond what any single blocklist can cover.
Understanding JEP 415 Object Filters
JEP 415 arrived in Java 17 and remains a cornerstone of Java security in 2026. It adds native support for deserialization filters via ObjectInputFilter—a built-in mechanism that allows you to inspect classes and their properties before they are instantiated. Instead of denying a fixed list, you define rules for what is allowed. This fundamental shift from deny-list to allow-list gives you the power to block gadget chains before they execute.
Filters operate at the point where a serialized stream is decoded. Before each class descriptor is resolved, the filter evaluates it against your rules and returns one of three verdicts: ALLOWED, REJECTED, or UNDECIDED. You can also choose to fail the entire stream if any rejected class is encountered, aborting the deserialization process immediately. This makes it possible to stop an attack before a single object is created.
There are three types of filters available in modern Java:
- Process-wide filters: Set via system properties or the
jdk.serialFilterproperty, applying to all streams in the JVM. - Per-stream filters: Applied to individual
ObjectInputStreaminstances, ideal for varying security requirements across different endpoints. - Object filter factories: Implemented to dynamically choose a filter based on the current stream or invocation context—useful for large, multi-tenant applications.
Configuring Object Filters in Your Java Applications
To actually prevent insecure deserialization in Java with filters, understanding configuration is key. The simplest start is a process-wide filter. Add a line to your JVM startup arguments or configure it in your deployment manifest:
-Djdk.serialFilter=java.base/java.util.*;java.base/java.lang.*;!*
This pattern allows only classes from java.util and java.lang (in the base module) and rejects everything else. In a typical enterprise application where only simple collections and strings are expected, this rule could block most known gadget chains outright.
However, strict allow-lists often break legitimate functionality. A more nuanced approach uses resource limits alongside class rules. For example:
-Djdk.serialFilter=maxarray=1000000;maxdepth=20;maxrefs=100000;java.base/*;!*
This filters out deeply nested objects that might indicate a hostile stream, caps array sizes, and limits object references—common signs of denial-of-service or exploit attempts. Combined with class allow-lists, these limits form a defensive net that is far more effective than a simple blocklist.
For application-specific needs, per-stream filters give finer control. Override ObjectInputStream and set the filter on the instance:
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(new MyAppFilter());
Object obj = ois.readObject();
This is particularly useful for parse operations where you know exactly what types are expected. Because the filter runs before any object is constructed, you can reject hostile code payloads even if they are hidden in obscure library classes.
Writing Custom Filters to Block Gadget Chains
Built-in patterns only get you so far. To confidently prevent insecure deserialization in Java with filters, you must write custom filter logic tailored to your application’s object graph. A robust custom filter typically combines class-based checks, depth controls, and stream size validation.
Start with a factory method that returns a Class-aware filter:
public static ObjectInputFilter createAppFilter() {
return filterInfo -> {
Class<?> clazz = filterInfo.serialClass();
if (clazz == null) {
return ObjectInputFilter.Status.ALLOWED; // stream end marker
}
// Allow only known DTOs
if (clazz.getPackageName().startsWith("com.example.dto")) {
return ObjectInputFilter.Status.ALLOWED;
}
// Explicitly block dangerous types
if (gadgetClassSet.contains(clazz.getName())) {
return ObjectInputFilter.Status.REJECTED;
}
// Never allow arbitrary classes
return ObjectInputFilter.Status.REJECTED;
};
}
While a allow-list is more secure, some applications rely on third-party libraries that deserialize their own types. In that scenario, a hybrid approach works best: allow known safe libraries, reject anything outside a curated set, and always cap depth and references. This is exactly what JEP 415 was designed for—giving you a single, effective funnel to gate what enters the JVM.
Another powerful technique is to enforce stream constraints before class checks. For instance, rejecting streams deeper than 10 levels or with more than 1000 references can neutralize many exploit payloads that rely on deeply nested objects to trigger gadget chains.
Testing and Maintaining Your Deserialization Defenses
Even the best filters can fail if they are misconfigured or become outdated. Testing your rules against known gadget chains is an essential part of the development cycle. Many security teams now incorporate deserialization attack payloads into their CI/CD pipelines, using tools like ysoserial and custom fuzzers to verify that filters block the correct classes and patterns.
Consider adding a regression test that attempts to deserialize a payload containing CommonsCollections6 or a similar gadget. Your filter should reject it, and the test should assert that an InvalidClassException is thrown. This practice ensures that changes to your dependencies or filter rules do not silently widen the attack surface.
Maintenance goes beyond tests. As new gadget chains are published, review your filter’s ignore lists and allow-list entries. JEP 415 filters are lightweight, so they can be updated and redeployed frequently. In a zero-trust world, treat deserialization boundaries like external network ports: never assume that a filter written last year is still effective today.
Finally, integrate your deserialization filter strategy with broader security controls, such as using the Java Security Manager (where supported) and enforcing least-privilege execution for application containers. The combination of object filters and environment hardening makes it far harder for an attacker to chain a deserialization bug into a full remote code execution.
Conclusion
Insecure deserialization remains one of the highest-severity risks in Java applications, but with JEP 415 you have a powerful and modern defense. Moving from blocklist thinking to allow-list filters, enforcing resource limits, and writing custom filter logic can effectively prevent insecure deserialization in Java with filters in ways that older techniques cannot. By embracing these principles now, you not only fix today’s vulnerabilities but also future-proof your software against tomorrow’s gadget chains. The key is to treat deserialization streams as untrusted input and make your filters an active component of your security architecture, not a passive checklist item.
