Java Serialization

serializationJavaobject-persistenceRMIsecurity

Java Serialization

Java Serialization

Overview

Java Serialization is a built-in mechanism in the Java standard library that converts the state of Java objects into a byte stream, which can later be used to reconstruct the original objects. It is used for object persistence, network communication, and data exchange in distributed systems. Objects can be made serializable by implementing the java.io.Serializable interface.

Details

Java Serialization was introduced in Java 1.1 and consists of the following key components:

Core Classes and Interfaces

  • Serializable: A marker interface that indicates a class can be serialized
  • ObjectOutputStream: Class for writing objects to a byte stream
  • ObjectInputStream: Class for reading objects from a byte stream
  • Externalizable: Interface for when more fine-grained control is needed

Important Concepts

  • serialVersionUID: A unique identifier used for class version control
  • transient: Keyword to exclude fields from serialization
  • readObject/writeObject: Methods for implementing custom serialization logic
  • readResolve/writeReplace: Special methods for controlling the serialization process

Security Considerations

Java Serialization poses serious security risks:

  1. Deserialization attacks: Malicious data can execute arbitrary code when deserialized
  2. Class loading vulnerabilities: Data-controlled class instantiation becomes a breeding ground for attacks
  3. Untrusted data dangers: Serialized data from external sources should always be considered dangerous

Advantages and Disadvantages

Advantages

  • Java standard feature: Available without additional libraries
  • Complete object graph preservation: Can save complex object structures including references
  • RMI integration: Native support for Remote Method Invocation
  • Simple implementation: Basic functionality available just by implementing Serializable interface
  • Version management: Class compatibility management through serialVersionUID
  • Customizable: Can implement custom serialization logic with readObject/writeObject

Disadvantages

  • Security risks: Serious vulnerability to deserialization attacks
  • Performance: Slower compared to JSON or Protocol Buffers
  • Java-only: No interoperability with other languages
  • File size: Binary format but not efficient
  • Difficult debugging: Binary format is not human-readable
  • Breaking changes: Class structure changes easily break compatibility
  • Not recommended: Many security experts recommend avoiding its use

Reference Pages

Code Examples

Hello World - Basic Serialization

import java.io.*;

// Serializable class
class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        Person person = new Person("John Doe", 30);
        
        // Serialization
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
            System.out.println("Object serialized: " + person);
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // Deserialization
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("person.ser"))) {
            Person deserializedPerson = (Person) ois.readObject();
            System.out.println("Object deserialized: " + deserializedPerson);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Using the transient Keyword

import java.io.*;

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String username;
    private transient String password; // Excluded from serialization
    private String email;
    
    public User(String username, String password, String email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }
    
    @Override
    public String toString() {
        return "User{username='" + username + 
               "', password='" + password + 
               "', email='" + email + "'}";
    }
}

public class TransientExample {
    public static void main(String[] args) throws Exception {
        User user = new User("user123", "secretPassword", "[email protected]");
        
        // Serialization
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(user);
        oos.close();
        
        // Deserialization
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        User deserializedUser = (User) ois.readObject();
        ois.close();
        
        System.out.println("Original user: " + user);
        System.out.println("Deserialized user: " + deserializedUser);
        // password field will be null
    }
}

Custom Serialization

import java.io.*;
import java.time.LocalDateTime;

class CustomSerializable implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String data;
    private transient LocalDateTime timestamp;
    
    public CustomSerializable(String data) {
        this.data = data;
        this.timestamp = LocalDateTime.now();
    }
    
    // Custom serialization
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject(); // Default serialization
        oos.writeObject(timestamp.toString()); // Save LocalDateTime as string
    }
    
    // Custom deserialization
    private void readObject(ObjectInputStream ois) 
            throws IOException, ClassNotFoundException {
        ois.defaultReadObject(); // Default deserialization
        String timestampStr = (String) ois.readObject();
        this.timestamp = LocalDateTime.parse(timestampStr);
    }
    
    @Override
    public String toString() {
        return "CustomSerializable{data='" + data + 
               "', timestamp=" + timestamp + "}";
    }
}

Implementing Externalizable Interface

import java.io.*;

class ExternalizableExample implements Externalizable {
    private String name;
    private int value;
    private transient String cachedData;
    
    // No-arg constructor required for Externalizable
    public ExternalizableExample() {}
    
    public ExternalizableExample(String name, int value) {
        this.name = name;
        this.value = value;
        this.cachedData = "Cached: " + name;
    }
    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // Fully controlled serialization
        out.writeUTF(name);
        out.writeInt(value);
        // cachedData is not saved
    }
    
    @Override
    public void readExternal(ObjectInput in) 
            throws IOException, ClassNotFoundException {
        // Fully controlled deserialization
        name = in.readUTF();
        value = in.readInt();
        // Rebuild cachedData
        cachedData = "Cached: " + name;
    }
    
    @Override
    public String toString() {
        return "ExternalizableExample{name='" + name + 
               "', value=" + value + 
               ", cachedData='" + cachedData + "'}";
    }
}

Security Enhancement with Serialization Filters

import java.io.*;

public class SecureDeserialization {
    public static void main(String[] args) throws Exception {
        // Allowlist-based filter configuration
        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
            "java.lang.*;java.util.*;!*" // Allow only java.lang and java.util
        );
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject("Safe string");
        oos.writeObject(42);
        oos.close();
        
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        
        // Apply filter
        ois.setObjectInputFilter(filter);
        
        try {
            String str = (String) ois.readObject();
            Integer num = (Integer) ois.readObject();
            System.out.println("Deserialization successful: " + str + ", " + num);
        } catch (InvalidClassException e) {
            System.err.println("Blocked by filter: " + e.getMessage());
        }
        ois.close();
    }
}

Serialization Proxy Pattern

import java.io.*;

// Safe serialization of immutable classes
public final class Period implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private final Date start;
    private final Date end;
    
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException("Start must be before end");
        }
    }
    
    // Serialization proxy
    private static class SerializationProxy implements Serializable {
        private static final long serialVersionUID = 1L;
        
        private final Date start;
        private final Date end;
        
        SerializationProxy(Period p) {
            this.start = p.start;
            this.end = p.end;
        }
        
        // Reconstruct Period instance from proxy
        private Object readResolve() {
            return new Period(start, end); // Validated by constructor
        }
    }
    
    // Write proxy instead of Period instance
    private Object writeReplace() {
        return new SerializationProxy(this);
    }
    
    // Prevent direct deserialization
    private void readObject(ObjectInputStream stream) 
            throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }
}