1. Core best practices for safe serialization
Serialization is like packing luggage at the airport: if you don’t know what’s inside and who you trust your suitcase to, you might get an unpleasant surprise at the security checkpoint. In Java, serialization makes it easy to save and restore objects, but it also opens the door to a range of attacks if data comes from untrusted sources.
The classic threat:
Serialization in Java can be unsafe. If an attacker provides a malicious stream, deserialization may lead to the worst consequences: from altering fields to executing unwanted code. This isn’t a textbook scare story — there have indeed been cases in Java history where attacks were built on this mechanism.
Why does this happen?
Deserialization isn’t just about restoring field values. A full-fledged object is created in the process: special methods may be called (for example, readObject, readResolve), and sometimes vulnerable spots in the code may be hit via reflection. Classes from third-party libraries are especially dangerous: some perform actions right at deserialization time. Therefore, never trust serialized data obtained from the outside.
Use transient for sensitive data
If your class has fields that contain passwords, tokens, private keys, or other sensitive information, declare them as transient. This data will not go into the serialized stream.
import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // not serialized
// ...constructors, getters, setters...
}
What happens on deserialization? The password field will have the default value (null for strings). This is good: passwords won’t be stored in files or sent over the network.
Define serialVersionUID explicitly
Always specify serialVersionUID explicitly. This reduces the likelihood of compatibility errors and minimizes the risk of class substitution during deserialization.
private static final long serialVersionUID = 1L;
Why does this matter for security? If you don’t specify serialVersionUID, the compiler will generate it automatically based on the class structure. This may lead to unexpected mismatches and, in theory, to abuse via substituting classes with the same name but a different structure.
Validate object types during deserialization
Do not trust what came over the network or from a file. After deserialization, always verify that the received object has the expected type before working with it.
Object obj = objectInputStream.readObject();
if (obj instanceof User) {
User user = (User) obj;
// safely work with user
} else {
// unexpected type - throw an exception or handle the error
}
Why do this? A malicious stream may contain an object of a different class that implements Serializable but doesn’t match your business logic.
Restrict which classes can be deserialized (ObjectInputFilter)
Starting with Java 9, use filters — ObjectInputFilter — to restrict the set of classes that are allowed to be deserialized. It’s like a bouncer at the door.
Example: setting a filter
import java.io.*;
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;com.example.Address;!*"
);
ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);
Object obj = in.readObject(); // now only User and Address are deserialized
This filter allows only the User and Address classes of your application. All others will be blocked — an exception will be thrown. This significantly reduces the risk of a malicious object getting through.
Do not deserialize data from untrusted sources
The golden rule: if you are not sure about the data source, do not deserialize. Prefer formats that do not execute code while parsing (for example, JSON, or XML with safe parsers).
Bad practice example:
// Never do this with data from the internet!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // dangerous!
What should you do instead?
- Use JSON parsers (e.g., Gson/Jackson) or XML parsers with validation.
- If binary serialization is required, filter classes via ObjectInputFilter and validate types (instanceof).
Use alternative formats for integration with external systems
For integrations, use formats that do not execute code while parsing: JSON, XML, Protocol Buffers, etc. This almost eliminates deserialization-based attacks.
// Use a JSON parser instead of ObjectInputStream
User user = gson.fromJson(jsonString, User.class);
Do not store serialized objects in publicly accessible locations
Files with serialized objects may contain sensitive data. Do not store them in publicly accessible directories and restrict file system permissions.
Do not rely on serialization for integrity control
Serialization does not guarantee data integrity or authenticity. Use digital signatures, checksums, or encryption if changes are unacceptable.
2. Practice: ObjectInputFilter example and a vulnerability demo
Class filtering example
Suppose we have a User class:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password;
// ...constructors, getters, setters...
}
The filter allows only User:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;!*"
);
in.setObjectInputFilter(filter);
Now, if someone tries to slip in an object of a different class, deserialization will fail with an error.
Demonstrating a potential vulnerability
Malicious class:
// Imagine someone slipped in such a class
public class Evil implements java.io.Serializable {
static {
System.out.println("Malicious code executed!");
// anything could be here...
}
}
If you do not filter classes, during deserialization an Evil object can be created, and the static initializer will run when the class is loaded — this is a real attack.
4. Common mistakes when securing serialization
Error #1: Deserialization without filtering and type checking. Developers often read an object from a stream and immediately cast it to the desired type. This opens the door to attacks. Use ObjectInputFilter and validate the type with instanceof.
Error #2: Storing sensitive data without transient. If you forget to declare passwords/keys as transient, they will end up in the stream and may leak with the file.
Error #3: Missing serialVersionUID. Without an explicit serialVersionUID, unexpected compatibility errors and class substitution risks are possible.
Error #4: Using serialization for communication with external systems. Binary serialization is convenient inside an application (e.g., a cache) but dangerous for external exchange. Prefer JSON/XML/Proto with safe parsers.
Error #5: Ignoring data integrity. Changes to the bytes of a serialized file will go unnoticed. Apply digital signatures, checksums, or encryption.
GO TO FULL VERSION