I/O, Serialization & Networking
Serialization
Serializable, do it defensively.- Deserialization executes attacker-influenced code paths — a remote-code-execution class of bugs (EJ 85)
- Never deserialize untrusted bytes; use
ObjectInputFilterif you must serialVersionUID: declare it explicitly or refactoring breaks old datareadObjectis a constructor that skips your constructor — validate and copy defensively (EJ 88)- Prefer explicit formats: JSON (Jackson), protobuf, or a custom serialized form (EJ 87)
- Records serialize safely by construction — deserialization runs the canonical constructor
public class Session implements Serializable {
@Serial private static final long serialVersionUID = 1L;
private String user;
private transient SocketChannel channel; // transient: not written
}
try (var out = new ObjectOutputStream(Files.newOutputStream(path))) {
out.writeObject(session);
}
try (var in = new ObjectInputStream(Files.newInputStream(path))) {
Session s = (Session) in.readObject();
}Serialization preserves object graphs — shared references stay shared, cycles work — using serial numbers per stream. That power is also the attack surface: the byte stream decides which classes instantiate and with what field values, bypassing every constructor invariant you wrote (Immutability Class Design).
@Serial
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// copy mutable components (the stream may alias them elsewhere!)
start = new Date(start.getTime());
end = new Date(end.getTime());
// then validate invariants the constructor would have enforced
if (start.after(end)) throw new InvalidObjectException(start + " > " + end);
}For classes with meaningful invariants, the serialization proxy pattern (EJ Item 90) is cleaner: writeReplace swaps in a tiny immutable proxy record, whose readResolve reconstructs through the real constructor — invariants enforced by construction, no direct attack surface on the real class.