CodeGym /Courses /JAVA 25 SELF /Serialization security: best practices

Serialization security: best practices

JAVA 25 SELF
Level 43, Lesson 4
Available

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.

1
Task
JAVA 25 SELF, level 43, lesson 4
Locked
Wizard's Artifacts: Version Tag for Treasures
Wizard's Artifacts: Version Tag for Treasures
1
Task
JAVA 25 SELF, level 43, lesson 4
Locked
CRM Clients: Ensuring Data Integrity on Load
CRM Clients: Ensuring Data Integrity on Load
1
Survey/quiz
Serialization setup, level 43, lesson 4
Unavailable
Serialization setup
Serialization setup
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION