Serialization Security Risks and Alternatives

Naveen AnamaneniUpdated · 4 min read

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

RiskWhat can happen
Remote code executionCrafted bytes trigger "gadget chains" in classes on the classpath
Denial of serviceDeeply nested or huge object graphs use up memory or CPU
Bypassing validationFields of normal classes are set without the constructor's checks
Exposing secretsPrivate fields, including passwords, are written in readable form unless transient
Fragile compatibilitySmall 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.

FilterDemo.java▶ Try it
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());
        }
    }
}
Output
Allowed: Student[name=Navika, marks=91]
Rejected: filter status: REJECTED

Explanation: 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

FormatStrengthsTypical tools
JSONHuman-readable, works with every language, safe to parseJackson, Gson, Jakarta JSON-B
XMLSelf-describing, schemasJAXB, Jackson XML
Protocol Buffers / AvroCompact, fast, versioned schemasprotobuf-java, Avro
DatabaseQueries, transactions, sharingJDBC, JPA
Plain text / CSVSimple, editableFiles, 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:

JsonSketch.java
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

  1. Easy: Change the filter in FilterDemo to also allow java.util.ArrayList, and confirm that the list is now accepted.
  2. Harder: Write a toCsv and fromCsv pair for a Student record using only String methods, as a simple alternative to Java serialization.

Quick check: Serialization security

4 questions
1Why is deserializing untrusted data dangerous?
2What does ObjectInputFilter do?
3In a filter pattern, what does !* mean?
4Which is a common safe alternative for data exchange?

Key Takeaways

Was this tutorial helpful?

Lesson 376 of 482