Serialization Security Risks and Alternatives
Java serialization is convenient, but it has a serious weakness: deserializing data from an untrusted source can be dangerous. During readObject, Java creates objects of whatever classes the bytes name and runs their special methods. An attacker who controls the bytes can chain together existing library classes, called gadgets, to run harmful code on the server. This has caused many real security incidents. In this lesson, you will learn the risks, how to reduce them with ObjectInputFilter, and why modern code prefers formats like JSON.
The Risks
| Risk | What can happen |
|---|---|
| Remote code execution | Crafted bytes trigger "gadget chains" in classes on the classpath |
| Denial of service | Deeply nested or huge object graphs use up memory or CPU |
| Bypassing validation | Fields of normal classes are set without the constructor's checks |
| Exposing secrets | Private fields, including passwords, are written in readable form unless transient |
| Fragile compatibility | Small class changes break old data |
Reducing the Risk: ObjectInputFilter
Java 9 added ObjectInputFilter, which checks every class and size limit before an object is created. A filter can be written as a pattern: allowed classes are listed, and !* rejects everything else.
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
public class FilterDemo {
record Student(String name, int marks) implements Serializable { }
static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(o);
}
return bytes.toByteArray();
}
static Object read(byte[] data) throws IOException, ClassNotFoundException {
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"FilterDemo$Student;java.lang.String;maxdepth=5;!*");
try (ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(data))) {
in.setObjectInputFilter(filter);
return in.readObject();
}
}
public static void main(String[] args)
throws IOException, ClassNotFoundException {
Student s = new Student("Navika", 91);
System.out.println("Allowed: " + read(serialize(s)));
try {
read(serialize(new ArrayList<>()));
} catch (InvalidClassException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}Allowed: Student[name=Navika, marks=91]
Rejected: filter status: REJECTEDExplanation: The filter allows only the Student record and String, limits the graph depth to 5, and rejects everything else with !*. A Student passes. An ArrayList isn't on the list, so deserialization stops with InvalidClassException before the object is created. A JVM-wide filter can also be set with the jdk.serialFilter system property.
Alternatives to Java Serialization
| Format | Strengths | Typical tools |
|---|---|---|
| JSON | Human-readable, works with every language, safe to parse | Jackson, Gson, Jakarta JSON-B |
| XML | Self-describing, schemas | JAXB, Jackson XML |
| Protocol Buffers / Avro | Compact, fast, versioned schemas | protobuf-java, Avro |
| Database | Queries, transactions, sharing | JDBC, JPA |
| Plain text / CSV | Simple, editable | Files, String.split |
A JSON library maps objects to text and back without running arbitrary code from the data. With Jackson, for example, writing and reading a record looks like this:
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(new Student("Navika", 91));
// {"name":"Navika","marks":91}
Student back = mapper.readValue(json, Student.class);Jackson is an external library, so this snippet needs it on the classpath; build tools that add libraries are covered later in the course.
Common Mistakes
Accepting serialized objects in a web request, cookie or message queue. It's a classic attack entry point.
Relying on the cast after readObject for safety. The danger happens inside readObject, before the cast.
Serializing passwords and tokens. Mark them transient, or don't store them.
Interview Corner
Why is Java deserialization considered dangerous, and what are the alternatives?
ObjectInputStream.readObject instantiates whatever serializable classes the stream names and invokes their deserialization methods, so attacker-controlled bytes can combine existing classes on the classpath into gadget chains that execute code, exhaust resources or create objects that skip constructor validation. Mitigations include never deserializing untrusted data, using ObjectInputFilter allow-lists and limits (Java 9+), and preferring records. For data exchange, formats such as JSON, XML or Protocol Buffers, handled by libraries like Jackson, are safer and language-neutral.
Practice
- Easy: Change the filter in
FilterDemoto also allowjava.util.ArrayList, and confirm that the list is now accepted. - Harder: Write a
toCsvandfromCsvpair for aStudentrecord using onlyStringmethods, as a simple alternative to Java serialization.
Key Takeaways
Was this tutorial helpful?