What is JEP 290, and how do ObjectInputFilter and setObjectInputFilter let you constrain which classes are deserialized?
answer
- JEP 290 = built-in filter, check class BEFORE instantiation = look-ahead
- ObjectInputFilter.checkInput -> ALLOWED / REJECTED / UNDECIDED
- FilterInfo: serialClass + depth/refs/bytes/arrayLength (DoS too)
- install: setObjectInputFilter (per-stream) / jdk.serialFilter (global) / JEP 415 factory
- allowlist + trailing !* ; allowlist beats blocklist
basics
~20 sJEP 290 added a built-in filter to Java that runs while reading a serialized stream and decides, class by class, whether each class is allowed before it is created. You configure an allowlist so only expected classes pass; everything else is rejected, blocking most gadget-chain attacks.
solid answer
~40 sJEP 290 ('Filter Incoming Serialization Data', Java 9, backported to 8u121/7u131/6u141) introduced `ObjectInputFilter`: a callback the deserializer consults *before* instantiating each class in the stream. The filter receives a `FilterInfo` (the candidate class, plus stream metrics: depth, references, array length, total bytes) and returns ALLOWED, REJECTED, or UNDECIDED; a REJECTED class throws InvalidClassException, stopping the attack before any gadget is built — this is the engine behind look-ahead deserialization. You set it per-stream with `ObjectInputStream.setObjectInputFilter(...)`, or process-wide via the `jdk.serialFilter` system/security property, or programmatically via `ObjectInputFilter.Config`. Java 17 adds JEP 415 context-specific filter factories. Best practice is an **allowlist** ('reject everything except these classes/packages'), pinned with resource limits (maxdepth, maxrefs, maxbytes) to also blunt resource-exhaustion bombs. Patterns use a `;`-separated spec like `com.example.*;java.base/*;!*`.
code
java · 13 lines// Per-stream allowlist: accept only our DTOs and a few JDK types,
// reject everything else, and cap resources to stop bombs.
var filter = ObjectInputFilter.Config.createFilter(
"maxdepth=20;maxrefs=1000;maxbytes=100000;" +
"com.example.dto.**;java.util.*;java.lang.*;!*"); // !* = reject the rest
try (var ois = new ObjectInputStream(input)) {
ois.setObjectInputFilter(filter);
Object o = ois.readObject(); // a disallowed class -> InvalidClassException
}
// Programmatic equivalent of UNDECIDED -> allowed by default,
// which is why the trailing !* is mandatory for a real allowlist.go deeper
Knows Java has a way to restrict which classes can be deserialized (a filter) as a safety measure.
Can set a per-stream filter or jdk.serialFilter and explains it checks classes before they're created.
Designs minimal allowlists ending in !*, adds resource limits, knows the ALLOWED/REJECTED/UNDECIDED contract and where to install filters, and explains look-ahead vs the old resolveClass approach.
Rolls out filtering org-wide (process default + per-stream + JEP 415 factories), pairs it with migrating off native serialization and gadget-surface reduction, and adds monitoring/logging of rejections.
## The problem JEP 290 solves Native Java deserialization is dangerous because `ObjectInputStream.readObject()` instantiates whatever classes the *stream* names, running their hook methods (`readObject`, `readResolve`, …) during reconstruction. By the time your code sees the result and casts it, gadget classes have already executed. The defensive idea is **look-ahead deserialization**: inspect the class name in the stream *before* the object is constructed, and refuse classes you don't expect. Before Java 9 you implemented this by subclassing `ObjectInputStream` and overriding `resolveClass(ObjectStreamClass desc)` to check `desc.getName()`. **JEP 290** made this a first-class, built-in mechanism. ## What JEP 290 added (Java 9; backported to 8u121, 7u131, 6u141) A standard interface, `java.io.ObjectInputFilter` (it lived as `sun.misc.ObjectInputFilter` in the 8u backport). The deserializer calls the filter **for each new class and for stream metrics** as it reads, *before* instantiation. ### The filter contract The filter is a functional interface: ```java Status checkInput(FilterInfo info); ``` It returns one of: - **ALLOWED** — accept this element. - **REJECTED** — reject; the stream read fails with `InvalidClassException` (or a filter-status exception). No object of a rejected class is created. - **UNDECIDED** — defer; the *next* filter / the built-in checks decide. (If the chain ends UNDECIDED, the element is allowed by default — so an allowlist must end with an explicit reject like `!*`.) ### `FilterInfo` — what the filter can see - `serialClass()` — the candidate class (may be null for pure metric checks). - `arrayLength()` — length of an array being created (for array bombs). - `depth()` — current graph depth. - `references()` — number of back-references so far. - `streamBytes()` — bytes consumed so far. Those metrics let a filter also stop **resource-exhaustion / DoS bombs** (deeply nested or hugely referenced graphs), not just RCE gadgets. ## How to install a filter ### 1. Per-stream (most precise — recommended) ```java var ois = new ObjectInputStream(input); ois.setObjectInputFilter( ObjectInputFilter.Config.createFilter( "com.example.dto.*;java.lang.*;!*")); Object o = ois.readObject(); ``` Apply the *narrowest* allowlist that the specific call site needs. ### 2. Process-wide default Set the system/security property `jdk.serialFilter` (e.g. `-Djdk.serialFilter=...`), or call `ObjectInputFilter.Config.setSerialFilter(...)` once at startup. This catches streams you didn't write the filter for (libraries, RMI, JMX). ### 3. JEP 415 (Java 17): context-specific filter factories A *filter factory* (`ObjectInputFilter.Config.setSerialFilterFactory`) lets you compose/select a filter per deserialization context — useful when one app legitimately deserializes different shapes in different places. ## The pattern syntax (allowlist vs blocklist) A filter string is a `;`-separated list of patterns evaluated in order: - `com.example.*` — allow classes in that package (one level); `com.example.**` includes subpackages. - `java.base/*` — module-qualified. - `!org.apache.commons.collections.**` — a leading `!` **rejects** matches (blocklist entry). - `maxdepth=20;maxrefs=500;maxbytes=10000;maxarray=1000` — resource limits. - A trailing `!*` rejects everything not previously allowed — this turns the spec into a true **allowlist**. **Allowlist beats blocklist.** A blocklist (`!commons-collections;...`) only stops *known* gadgets; new gadget chains keep appearing. An allowlist (`only my DTOs + !*`) rejects everything unexpected by default, so unknown future gadgets are also blocked. Always prefer allowlisting plus resource caps. ## How it fits the bigger picture - It is **defense in depth**, not a license to deserialize untrusted data freely. The strongest control is still: don't use native serialization across trust boundaries — use a data-only format (JSON/Protobuf) into known types. - Filters can't see *inside* a class's logic; they gate *which classes* and *stream shape*. A class you allowlist could still misbehave, so keep the allowlist minimal. - Pre-Java-9 codebases (or libraries you can't change) can still subclass `ObjectInputStream.resolveClass` for the same look-ahead effect. **Summary:** JEP 290 turns look-ahead deserialization into a built-in, composable filter that runs before each class is instantiated. Configure a minimal allowlist ending in `!*`, add resource limits, and set both per-stream and process-wide defaults.
- Why is an allowlist preferred over a blocklist for serialization filters?A blocklist only blocks known gadget classes; new gadget chains keep being discovered. An allowlist rejects everything not explicitly expected (end it with !*), so unknown/future gadgets are blocked by default.
- Besides class names, what else can an ObjectInputFilter limit?Stream shape/resource metrics from FilterInfo: maxdepth, maxrefs, maxbytes, and array length — letting it reject deeply nested or huge graphs (deserialization 'bombs'), mitigating DoS as well as RCE.
- How did people do look-ahead before JEP 290?Subclass ObjectInputStream and override resolveClass(ObjectStreamClass desc) to inspect desc.getName() and throw for disallowed classes before they're loaded/instantiated.
saying these in an interview costs you the question
- Writing a blocklist of known gadgets and calling it secure.
- Forgetting the trailing !*, so UNDECIDED defaults to allowed and the allowlist leaks.
- Believing a filter makes deserializing untrusted data fully safe (it's defense in depth).
- Thinking the filter runs after the object is built (it runs before instantiation).