How do I serialize and deserialize objects safely?

In a modern Java environment, “safely” means avoiding the pitfalls of standard Java Serialization, which is often vulnerable to gadget attacks and remote code execution.

Here are the best practices for safe serialization and deserialization:

1. Prefer Data-Only Formats (JSON/XML)

Instead of standard Java serialization, use JSON with a library like Jackson (already common in Spring projects). It separates data from logic, making it much harder for an attacker to trigger malicious code during deserialization.

Example using Jackson:

import com.fasterxml.jackson.databind.ObjectMapper;

public class SerializationDemo {
    private final ObjectMapper mapper = new ObjectMapper();

    public String serialize(Object obj) throws Exception {
        return mapper.writeValueAsString(obj);
    }

    public <T> T deserialize(String json, Class<T> clazz) throws Exception {
        // Safe because it only maps data to fields in the specified class
        return mapper.readValue(json, clazz);
    }
}

2. If You Must Use Java Serialization: Use Filtered Deserialization

If you are forced to use java.io.Serializable, you should implement a SerializationFilter. Introduced in Java 9 (and perfected in later versions), this allows you to “allowlist” only the classes you expect.

Example of an ObjectInputFilter:

import java.io.*;

public class SafeDeserializer {
    public static Object deserialize(byte[] data) throws IOException, ClassNotFoundException {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
             ObjectInputStream ois = new ObjectInputStream(bais)) {

            // Allow ONLY specific classes (and primitives/arrays)
            ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
                "com.yourpackage.MySafeClass;java.base/*;!*"
            );
            ois.setObjectInputFilter(filter);

            return ois.readObject();
        }
    }
}

3. Use transient for Sensitive Data

Always mark fields that shouldn’t be serialized (like passwords, tokens, or internal state) as transient.

public class User implements Serializable {
    private String username;
    private transient String password; // Will not be saved/transmitted
}

4. Implement readObject for Validation

If you use standard serialization, override readObject to validate the object’s state after it is reconstructed. This prevents “half-baked” or illegal objects from being created.

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ois.defaultReadObject();
    // Validate state
    if (this.age < 0) {
        throw new InvalidObjectException("Age cannot be negative");
    }
}

Summary of “Safe” Rules:

  1. Don’t accept serialized objects from untrusted sources.
  2. Use JSON/Jackson whenever possible (it’s the industry standard for a reason).
  3. Use allowlists (via ObjectInputFilter) if you use native Java serialization.
  4. Keep dependencies updated to patch known “gadget” classes that attackers use to exploit deserialization.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.