1. Binary serialization security
Serialization in Java is not just saving an object’s fields—it allows you to “recreate” any object with any content, as long as it implements the Serializable interface. It might seem convenient! But if your application deserializes data obtained from an untrusted source (for example, from the network or from a file an attacker could have tampered with), it risks becoming the victim of attacks.
How does it work?
During deserialization, Java creates objects from a byte stream without invoking class constructors. If a class implements special methods like readObject, they will be called automatically. An attacker can craft the byte stream so that deserialization executes vulnerable code.
Example: “gadget chain” attack
Imagine you have a class that, during deserialization, launches an external command (reads a file or invokes a shell). If an attacker knows that the application deserializes objects of certain types, they can inject a specially crafted byte stream that results in the execution of malicious code. Such attacks are built as a “gadget chain”—a sequence of calls that ends in a dangerous operation.
Why is this so critical?
Deserialization is a process in which real objects are reconstructed from a data stream, and at that moment arbitrary code may execute. If the data comes from an untrusted source, it opens the door to remote command execution (RCE). Major companies have published recommendations to abandon unsafe deserialization; in modern enterprise projects, binary serialization is often prohibited by security policies.
How to defend?
- Never deserialize objects from untrusted sources.
- Use whitelisting—an explicit list of permitted types for deserialization.
- Prefer text formats for external integrations: JSON, XML.
- If serialization is unavoidable, use libraries with secure deserialization settings (for example, Jackson with type restrictions).
- Limit the use of non-standard serialization methods (readObject, readResolve, etc.) unless you are confident in their safety.
2. Class version compatibility
Binary serialization in Java is tightly coupled to the class structure. You serialized an object in version 1.0, then updated the class (added/removed a field)—attempting to deserialize the “old” object into the new version may lead to an error or data loss.
How does Java determine compatibility?
A special field—serialVersionUID—is used for this. It is the class version identifier. If the serialized object has one serialVersionUID and the current class has another, an InvalidClassException will be thrown and deserialization will not take place.
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // explicitly set the version
private String name;
private int age;
}
If you change the class structure (for example, add the email field) and do not change the serialVersionUID, Java will consider the class compatible and will try to deserialize the old object. If you have not specified serialVersionUID explicitly, the JVM will generate it automatically based on the structure, and any change will lead to incompatibility.
What happens when versions mismatch/match?
If the identifiers do not match, deserialization will not occur: InvalidClassException. If they match, fields are mapped by name and type: new fields receive default values (null, 0), removed fields are ignored. Changing a field’s type or name may cause errors and incorrect interpretation of data.
Practical tip. Always set an explicit serialVersionUID in serializable classes. Change it only for incompatible changes (removing/changing the type of an important field). When adding new fields, you can keep the identifier the same—the JVM will handle old objects correctly.
Table: What happens when the class changes
| Change in the class | What happens during deserialization? |
|---|---|
| Added a new field | Gets the default value (0, null) |
| Removed a field | Ignored when reading old data |
| Changed the field type | Exception or incorrect data |
| Renamed the field | Old field is ignored, new one gets the default value |
| Changed serialVersionUID | InvalidClassException |
3. Limitations of standard serialization
Not all objects can be serialized
Fields with the transient and static modifiers are not serialized. static—because it belongs to the class rather than the object; transient—because you explicitly forbade the field’s serialization.
Some objects are by definition not serializable: Thread, database connections, sockets, Scanner, etc. If your class has a field of such a type and it is not transient, you will get a NotSerializableException.
import java.io.Serializable;
import java.util.Scanner;
public class Session implements Serializable {
private transient Scanner scanner; // not serialized!
private String login;
}
Performance and extensibility issues
Serializing large object graphs can be slow and memory-intensive.
The binary format is a poor fit for integrations with other platforms and languages—it’s understood only by Java.
It’s hard to control exactly what gets serialized, especially with deep hierarchies and cyclic references.
Problems with legacy data support
Long-term storage of binary snapshots is risky. After a year or two the class structure changes, and old files stop loading.
A real story: “We saved a serialized user cache three years ago, updated the application, and now we can’t load it. Hello, lost data!”
4. Best practices: how to avoid pitfalls
- Use binary serialization only for internal tasks where you control both sides of the process.
- Do not use binary serialization for external integrations or long-term storage of important data.
- Always specify serialVersionUID explicitly in serializable classes.
- Mark fields that should not be serialized with the transient modifier.
- For interaction with external systems—use text formats and modern libraries: JSON, XML, Jackson, Gson, JAXB.
- For compatibility, use versioning: store the object version in the class itself and adapt handling during deserialization.
- If serialization is only for cache—do not maintain compatibility at all costs; it’s easier to recompute the cache.
- Do not keep sensitive data (passwords, keys) in serializable objects—serialization does not encrypt data.
5. Common mistakes when working with binary serialization
Mistake #1: Deserializing data from an untrusted source. The most dangerous mistake is to accept and deserialize objects coming from the outside (from the network, from a user, from a tampered file). This is a direct path to vulnerabilities up to and including RCE.
Mistake #2: Implicitly changing the class structure without updating serialVersionUID. If you do not specify the identifier explicitly, the JVM will generate it automatically. Any structural change (even the order of fields) will lead to incompatibility and make it impossible to load old objects.
Mistake #3: Attempting to serialize objects with non-serializable fields. If a class has a field that does not implement Serializable and it is not transient, serialization will end with an exception.
Mistake #4: Storing temporary or sensitive data in serializable objects. Tokens, passwords, temporary resource descriptors—all of this may accidentally end up in a file.
Mistake #5: Using binary serialization for long-term storage and cross-version exchange. After the first class update there is a high risk of “corrupted” data and compatibility problems.
Mistake #6: Expecting that static and transient fields will “come back” after deserialization. These fields are not serialized; after loading they will have default values.
GO TO FULL VERSION