Java Serialization
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:
- Deserialization attacks: Malicious data can execute arbitrary code when deserialized
- Class loading vulnerabilities: Data-controlled class instantiation becomes a breeding ground for attacks
- 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
- Java Object Serialization Specification (Oracle)
- Secure Coding Guidelines for Java SE (Oracle)
- Java Serialization Documentation
- Effective Java - Serialization Chapter
- OWASP - Deserialization Cheat Sheet
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");
}
}