Serialisation in Java is the process of converting an object into a byte stream so it can be stored or sent, and deserialisation reverses it, but deserialising data you do not control is one of the most dangerous things a Java application can do. The mechanism itself is ordinary. The danger is that ObjectInputStream.readObject() will reconstruct almost any class on the classpath, and attackers have turned that flexibility into reliable remote code execution.
If you take one thing from this guide: never call readObject() on bytes that came from a user, a queue, a cookie, or any source outside your trust boundary.
How Java serialisation works
A class opts into serialisation by implementing the marker interface Serializable. The JVM then handles writing and reading its fields automatically:
public class Session implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private int roleId;
}
Writing and reading look like this:
// Serialize
try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
oos.writeObject(session);
}
// Deserialize — this is the dangerous call on untrusted input
try (ObjectInputStream ois = new ObjectInputStream(in)) {
Session s = (Session) ois.readObject();
}
The serialVersionUID field is a version stamp. If it does not match between the writer and reader, deserialisation throws InvalidClassException. Fields marked transient are skipped, which is the intended way to keep secrets out of the byte stream.
Why deserialising untrusted data is dangerous
The problem is that readObject() does not just populate fields. As it reconstructs an object graph, it invokes methods like readObject, readResolve, and finalize on the classes being instantiated. An attacker cannot inject new code, but they can choose which existing classes get instantiated and in what order.
By chaining together methods across libraries that happen to be on the classpath, an attacker assembles a gadget chain: a sequence of ordinary method calls that ends in something dangerous, like Runtime.exec(). The attacker sends a crafted byte stream, your app deserialises it, and the chain fires during reconstruction, before your code ever runs a single line of validation.
This is not theoretical. The 2015 "Java deserialization apocalypse" showed gadget chains in Apache Commons Collections that hit WebLogic, JBoss, Jenkins, and many others. Tooling like ysoserial packages ready-made chains for dozens of common libraries, which means the attacker does not even need to find a new gadget; they just need your app to deserialise their bytes.
The JSON angle: it is not only native serialisation
Teams often think switching from native serialisation to JSON solves the problem. It does not automatically. Libraries that support polymorphic deserialization, where the incoming data names the concrete class to instantiate, reintroduce the same gadget risk. Jackson's jackson-databind had a long series of CVEs stemming from its default-typing feature: CVE-2017-7525 opened the door, and roughly thirty follow-on CVEs were filed as new gadget classes were discovered, each requiring an addition to Jackson's blocklist.
The lesson is that any deserialiser that lets the input choose the target type is a candidate for abuse. If you use Jackson, keep enableDefaultTyping() off, keep the library patched, and prefer explicit type binding. We have a deeper walkthrough in the Jackson databind security guide.
Defending native serialisation
If you must use native Java serialisation, apply these controls in order of impact.
Use a serialisation filter (JEP 290). Java 9 introduced ObjectInputFilter, and it was backported to later 8 updates. It lets you allowlist the classes that may be deserialised and reject everything else before the object is constructed:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.model.*;java.base/*;!*");
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(filter);
The pattern above allows your model package and core JDK classes, then denies all others with !*. An allowlist is far safer than a blocklist because you do not have to predict every gadget.
Prefer data formats without code semantics. For crossing a trust boundary, a plain data format like JSON or Protocol Buffers with strict, explicit type binding avoids the whole gadget class of problems. You control exactly which fields map to which types.
Add integrity checks. If serialised state must travel through a client (a signed cookie, for instance), sign it with an HMAC and verify the signature before deserialising. This does not fix a gadget chain from a trusted-but-compromised source, but it stops an outside attacker from submitting arbitrary bytes.
Run components with least privilege so a successful chain has a smaller blast radius, and keep dependencies patched. Many deserialisation CVEs are in transitive libraries you never chose directly; an SCA tool such as Safeguard can flag a vulnerable commons-collections or jackson-databind version pulled in transitively, which is exactly where these gadgets hide. The SCA product resolves the full tree so you see the version that actually ships.
A quick audit checklist
Search the codebase for readObject, ObjectInputStream, readUnshared, and enableDefaultTyping. For each hit, ask where the bytes come from. Anything reachable from a network endpoint, a message queue, a cache, or a client-supplied cookie needs a filter or a redesign. Endpoints that only deserialise data your own trusted services wrote are lower risk but still deserve a filter as defense in depth.
FAQ
Is Java serialisation being removed?
Not removed, but the JDK maintainers have long described the original design as a mistake and are working toward a safer replacement. In the meantime, serialisation filters (JEP 290) are the supported way to use it more safely. Treat native serialisation as legacy for anything crossing a trust boundary.
Does switching to JSON make deserialisation safe?
Not by itself. JSON libraries that support polymorphic type handling, such as Jackson with default typing enabled, can be exploited with the same kind of gadget chains. Disable default typing, use explicit type binding, and keep the library patched.
What is a gadget chain?
A gadget chain is a sequence of method calls across classes already on the classpath that an attacker triggers through deserialisation to reach a dangerous operation like command execution. The attacker supplies data, not code; they just steer which existing methods run.
How do I know which of my dependencies have deserialisation flaws?
Run software composition analysis against your resolved dependency tree. It maps each package version to known CVEs, including transitive ones like the jackson-databind and Commons Collections issues, so you can upgrade before the flaw is reachable in production.