skip to content

How do Java object input filters (JEP 290) mitigate deserialization attacks, and what are their limits?

level: seniorimportance: should knowfreq 48%

answer

  1. JEP 290 (Java 9), JEP 415 filter factory (Java 17)
  2. ALLOWED / REJECTED / UNDECIDED per class
  3. allowlist with trailing !* = deny by default
  4. maxdepth/maxrefs/maxbytes/maxarray for DoS
  5. mitigation, not a cure — allowed class can still be a gadget

basics

~20 s

Java lets you install a filter that checks each class in an incoming serialized stream before it is rebuilt, and you can reject any class not on an allowlist. It also caps things like depth and array size to stop resource-exhaustion attacks.

solid answer

~50 s

Object input filters, added in Java 9 (JEP 290) and extended in Java 17 (JEP 415, context/factory filters), let you intercept deserialization. A filter is a function that the JVM calls for each class about to be resolved and for stream metrics; it returns ALLOWED, REJECTED, or UNDECIDED. The recommended pattern is an **allowlist**: reject everything except the specific classes you expect (REJECTED for unknown classes), which collapses the gadget surface to near zero. You also cap maxdepth, maxrefs, maxbytes, and maxarray to block DoS. Filters can be set per-stream (setObjectInputFilter), via a JVM-wide property (jdk.serialFilter), or, in 17+, a per-context filter factory. Limits: a filter is a mitigation, not a cure — an allowlisted class can itself be a gadget; filters don't help non-Java formats; a too-broad pattern (e.g. allowing a whole package) reopens the hole; and they require knowing your legitimate class set. Removing native serialization remains the stronger fix.

code

java · 11 lines
java
ObjectInputStream ois = new ObjectInputStream(in);

// Allow only known-safe DTOs + a few JDK collections; reject everything else.
// Also cap depth/array size to block resource-exhaustion (DoS) payloads.
ObjectInputFilter allowlist = ObjectInputFilter.Config.createFilter(
    "maxdepth=20;maxarray=10000;" +
    "com.myapp.dto.*;java.util.ArrayList;java.lang.String;" +
    "!*");                       // trailing !* = REJECT anything not matched above
ois.setObjectInputFilter(allowlist);

MyDto dto = (MyDto) ois.readObject(); // unlisted classes -> InvalidClassException

go deeper

for a junior

Knows Java has a feature to block unwanted classes from being deserialized.

for a middle

Can describe installing a filter that allowlists expected classes and rejects others, plus setting size/depth limits.

for a senior

Explains ALLOWED/REJECTED/UNDECIDED semantics, per-stream vs JVM-wide vs factory installation, allowlist-over-denylist, and that an allowed class can still be a gadget.

for a principal

Treats filters as one layer in defense-in-depth, weighs them against removing native serialization entirely, and defines org policy (default jdk.serialFilter, dependency hygiene, format migration roadmap).

## The problem they solve Native Java deserialization rebuilds whatever classes the byte stream names, running their reconstruction logic before you can validate anything (see the 'why dangerous' question). The mitigation idea: **inspect the stream as it's being read and refuse to materialize disallowed classes**. That is exactly what an **object input filter** does. ## What a filter is Introduced in **Java 9 as JEP 290** (and present in 8u121+ via backport), a filter is an implementation of `ObjectInputFilter`. The JVM calls your filter's `checkInput(FilterInfo)` method: - **For every class** about to be resolved during deserialization (you get the `Class<?>` via `info.serialClass()`). - **For stream metrics**: current depth, number of references, stream size in bytes, array length being allocated. Your filter returns one of three statuses: - `Status.ALLOWED` — let it through. - `Status.REJECTED` — abort deserialization (throws `InvalidClassException`). - `Status.UNDECIDED` — defer to the next filter / default behavior. ## The allowlist pattern (the right way) The secure pattern is **deny by default**: explicitly ALLOW the handful of classes your endpoint legitimately expects, REJECT everything else. A pattern string makes this concise, e.g. `com.myapp.dto.*;java.util.ArrayList;!*` where the trailing `!*` rejects all not previously matched. This shrinks the attacker's reachable classes from 'everything on the classpath' to 'your few DTOs', which usually have no gadget potential. Alongside class checks, set numeric **resource limits** to stop denial-of-service payloads: `maxdepth` (nesting), `maxrefs` (object references), `maxbytes` (total stream size), `maxarray` (array element count). These stop 'billion laughs'-style streams that explode in memory before any class check matters. ## How to install one - **Per stream:** `ois.setObjectInputFilter(myFilter)` right after creating the `ObjectInputStream` — most targeted. - **Process-wide:** the JVM property/security property `jdk.serialFilter=...` applies a default filter to all streams. - **Java 17+ (JEP 415):** a **filter factory** — `ObjectInputFilter.Config.setSerialFilterFactory(...)` — lets you compose a context-specific filter per deserialization, so a library can layer its own filter on top of the application default. This fixed the gap where a single static filter couldn't adapt per call site. ## Limits and caveats (the senior nuance) 1. **An allowlisted class can still be a gadget.** If a class you must allow has a dangerous `readObject`, the filter won't save you — it only controls *which* classes, not what they do. 2. **Over-broad patterns reopen the hole.** Allowing a whole framework package (`org.somelib.**`) can readmit gadgets. Keep the list minimal. 3. **It is a mitigation, not elimination.** The authoritative guidance still says: **don't deserialize untrusted data at all**; prefer a data-only format (JSON/Protobuf). Filters are for cases where you're stuck with native serialization (legacy RMI/JMX, third-party blobs). 4. **No help for other formats.** A filter only governs `ObjectInputStream`; it does nothing for, say, an XML or JSON library configured to instantiate arbitrary types. 5. **You must know your legitimate set.** Maintaining an accurate allowlist requires understanding exactly what flows through each stream; a wrong list either breaks functionality or is too permissive. ## Bottom line Object input filters convert native deserialization from 'instantiate anything' to 'instantiate only what I named', plus enforce resource caps. That is a major risk reduction and the right tool when you cannot remove native serialization — but it sits *behind* the stronger architectural fix of not using native serialization on untrusted input.

  • Why is an allowlist preferred over a denylist for serial filters?
    A denylist must enumerate every dangerous class; new gadget chains are discovered constantly, so it's always behind. An allowlist permits only your known-good types, so unknown gadgets are rejected by default.
  • What problem did JEP 415's filter factory solve over JEP 290?
    JEP 290's single static/global filter couldn't be composed per call site. The factory lets each deserialization context build its own filter (e.g. a library layering on top of the app default), enabling context-specific allowlists.

saying these in an interview costs you the question

  • Using a denylist of 'known bad' classes instead of an allowlist — new gadgets appear constantly
  • Allowing an entire package (**) and assuming you're safe
  • Believing a filter makes native deserialization of untrusted data fully safe
  • Forgetting the resource limits, leaving DoS open even with a class allowlist

context