91. Serialization Pitfalls
Java serialization is dangerous: missing serialVersionUID causes InvalidClassException on class changes; transient fields are skipped; deserialization of untrusted data is a remote code execution vulnerability.
Explanation
Serializable marks a class for Java's binary object serialization. If serialVersionUID is not declared explicitly, the JVM computes it from the class structure โ add or remove a field and the UID changes, making old serialized data unreadable (InvalidClassException). Always declare private static final long serialVersionUID = ...;.
transient fields are excluded from serialization โ useful for caches, derived values, or sensitive data (passwords, keys). They are initialised to their default value (null, 0, false) upon deserialization.
The real danger: deserializing data from an untrusted source can execute arbitrary code. Attackers craft gadget chains through existing classes on the classpath (Apache Commons, Spring internals, etc.) that chain together via deserialization callbacks. This has been the source of critical CVEs (Apache Struts RCE, JBoss RCE, etc.). Use ObjectInputFilter (Java 9+), serialization proxies (Effective Java), or avoid Java serialization entirely โ use JSON/protobuf instead.
Example
import java.io.*;
public class SerializationPitfalls {
// CORRECT: explicit serialVersionUID prevents accidental breakage
static class GoodUser implements Serializable {
private static final long serialVersionUID = 1L; // explicit!
private String name;
private transient String password; // NOT serialized (security!)
private transient int cachedHash; // NOT serialized (derived)
GoodUser(String name, String password) {
this.name = name;
this.password = password;
}
// Custom deserialization: restore transient fields if needed
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // restore non-transient fields
this.cachedHash = name.hashCode(); // re-derive
// password remains null after deserialization (by design)
}
@Override public String toString() {
return "GoodUser{name='" + name + "', password=[" +
(password == null ? "not set" : "set") + "]}";
}
}
public static void main(String[] args) throws Exception {
GoodUser user = new GoodUser("Alice", "secret123");
System.out.println("Before: " + user);
// Serialize to bytes
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(user);
}
System.out.printf("Serialized: %d bytes%n", baos.size());
// Deserialize
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(baos.toByteArray()))) {
GoodUser restored = (GoodUser) ois.readObject();
System.out.println("After: " + restored);
// password is null โ transient field NOT restored
}
// Modern alternative: avoid Java serialization
// Use JSON (Jackson, Gson), Protocol Buffers, or records + JSON
System.out.println("\nPrefer: JSON or Protobuf over Java serialization");
System.out.println("Use ObjectInputFilter for any serialization of external data");
}
}Sources
- https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/Serializable.html โ Serializable API
- https://www.pearson.com/en-us/subject-catalog/p/effective-java/P200000000138 โ Effective Java, Items 85-87