Java ObjectInputStream and friends
ObjectInputStream.readObject() is the Java equivalent of
pickle: a payload of class metadata that the runtime
reconstructs, including invoking constructors, setting
fields, and triggering readObject / readResolve /
finalize on every class in the graph. The “gadget chains”
that make this exploitable have been written and re-written
since 2015; the CVE shelf is full.
Pattern
new ObjectInputStream(...).readObject()on any network / file / DB-blob input.- Frameworks that serialize via Java serialization under the hood: older RMI, JMS, JNDI lookup-with-RemoteObject paths, some session-replication backends.
- Legacy uses of
ObjectInputStreamfor “convenient” same-VM persistence — still dangerous if the file ever reaches a different VM with different classpath assumptions.
Why it matters
A malicious payload using a known gadget chain (commons-
collections, Spring, Groovy, the long-running Pwn-Twitter
list) can RCE on readObject() without the application code
ever being entered. There is no patch coming; the design
predates the threat model.
Mitigation — JEP 290 deserialization filters
Java 9+ exposes
ObjectInputFilter / ObjectInputFilter.Config (and the
older setObjectInputFilter API). Set a strict allowlist
filter on every ObjectInputStream the application creates:
ObjectInputFilter allowlist = ObjectInputFilter.Config.createFilter(
"com.example.Order;com.example.OrderItem;java.lang.Number;" +
"java.util.ArrayList;java.util.HashMap;" +
"!*" // reject everything else
);
try (ObjectInputStream in = new ObjectInputStream(input)) {
in.setObjectInputFilter(allowlist);
Object obj = in.readObject();
// ...
}Or set it globally at JVM start:
-Djdk.serialFilter='com.example.*;java.lang.Number;!*'. The
filter rejects every class outside the allowlist before the
class is loaded, before readObject is invoked, before any
gadget chain runs.
Uplift — replace ObjectInputStream entirely
For any new-write path, switch to a JSON serializer with explicit type handling:
- Jackson with default-typing disabled and
@JsonTypeInfo(use=Id.NAME)plus@JsonSubTypes({...})enumerations on polymorphic fields, or aBasicPolymorphicTypeValidatorallowlist if default typing genuinely cannot be removed. - Gson with explicit
TypeAdapter/RuntimeTypeAdapterFactoryregistrations. - Protobuf when the data is structured enough to warrant a schema.
Read paths for legacy data: keep an ObjectInputStream reader
with the JEP 290 filter installed until the persisted data
has been migrated; then remove the reader.
Inputs
- Call sites — every
new ObjectInputStream(...)and every framework call known to deserialize Java objects internally. - Strategy — mitigate / uplift / both.
The prompt
You are remediating Java deserialization call sites in this
repo. Output a PR or a TRIAGE.md.
## Step 0 — Inventory
1. List every `new ObjectInputStream(...)` in the repo.
2. List every framework usage that deserializes Java objects
internally — check for `ObjectMapper.readValue` with
`enableDefaultTyping`, RMI registrations, JMS message
listeners that trust message bodies, session-replication
bindings.
3. For each, record whether the input crosses a trust
boundary.
## Step 1 — Pick the strategy
- **All call sites:** mitigate by adding a JEP 290 filter.
This is required regardless of any uplift.
- **New-write paths:** also uplift to JSON with explicit
typing.
- **Legacy read paths:** keep the filtered ObjectInputStream
until data is migrated.
## Step 2 — Mitigate
1. Define a single allowlist filter as a constant in a
security utility class.
2. Apply the filter to every ObjectInputStream creation. If
the codebase has many call sites, factor a
`safeObjectInputStream(InputStream)` helper and migrate to
it.
3. Optionally, add the global JEP 290 filter to the
application's JVM args / Dockerfile / launcher.
4. Add tests:
- A payload containing an allowlisted class deserializes
successfully.
- A payload containing a non-allowlisted class
(`org.apache.commons.collections.functors.InvokerTransformer`,
`java.lang.Runtime`) is rejected.
## Step 3 — Uplift (when applicable)
1. Replace `ObjectOutputStream` writers with the chosen JSON
serializer.
2. Add a behaviour-preservation test that round-trips a
representative object through the old binary format and
the new JSON format and asserts field-by-field equality.
3. If the codebase persists data: add a one-time migration job
that reads old binary files via the filtered ObjectInputStream
and writes them as JSON. Document a removal date for the
legacy reader.
## Step 4 — Open the PR
- Branch: `remediate/java-deser-<module-slug>`.
- Title: `[Security][deserialization] add JEP 290 filter / uplift to JSON in <module>`.
- Body: call-site inventory, allowlist contents, test
additions, migration plan if applicable, and the JVM-args
change if global filter applied.
- Label: `sec-auto-remediation`.
## Stop conditions
- A framework is doing the deserialization and the agent
cannot inject a filter. (E.g., a third-party library that
exposes no filter hook — flag and triage.)
- Default typing in Jackson is load-bearing for a feature the
agent cannot reshape without an API change.
- Test coverage on the call path is too thin to detect
regressions safely.
## Scope
- Do not bundle in unrelated refactors.
- Do not silently broaden the allowlist.
- Do not remove the legacy reader without a documented
migration.Watch for
- Allowlist drift. A reviewer who adds a class to the allowlist next quarter without re-reading the gadget-chain list defeats the mitigation. Guard with a comment that says so explicitly, and re-review the allowlist quarterly.
enableDefaultTypingre-enabled. Jackson will resolve arbitrary classes if default typing is on. The Jackson recipe is a sibling pattern; if the codebase uses it, fix both at once.- JNDI lookups in deserialized payloads. Even with a strict filter, some allowed classes can re-trigger lookups — validate the allowlist against the log4j-style JNDI lookups threat surface.
- Globally setting the JVM filter can break other applications on the same JVM if any. Prefer per-stream filters when the JVM hosts multiple applications.
Related
- Classic Vulnerable Defaults — workflow context.
- Python pickle — same risk class in Python.