Secure Coding Standards for Java Serialization

1. Overview

In this post, I would like to explain the basic coding standard rules that can follow while using Serialization in Java Projects.
This post belongs to my favorite Java Best Practices Series category. Before using Serialization in Java Projects, I suggest you read this basic coding standard rule to develop good coding practices.

2. Standard Coding Rules for Java Serialization

2.1 Enable serialization compatibility during class evolution

Classes that implement a Serializable interface without overriding its functionality are said to be using the default serialized form.
Consider using custom serialized form and don’t use the default serialized form. Once an object of a particular class has been serialized, future refactoring of the class's code often becomes problematic. Specifically, existing serialized forms (encoded representations) become part of the object's published API and must be supported for an indefinite period.
If the class does not provide a serialVersionUID, the Java Virtual Machine (JVM) assigns it one using implementation-defined methods.
Let's understand this rule with problematic code and solution code examples.
Problematic Code Example
This problematic code example implements a GameWeapon class with a serializable field called numOfWeapons and uses the default serialized form. Any changes to the internal representation of the class can break the existing serialized form.
class GameWeapon implements Serializable {
  int numOfWeapons = 10;
         
  public String toString() {
    return String.valueOf(numOfWeapons);
  }
}
Because this class does not provide a serialVersionUID, the Java Virtual Machine (JVM) assigns it one using implementation-defined methods. If the class definition changes, the serialVersionUID is also likely to change. Consequently, the JVM will refuse to associate the serialized form of an object with the class definition when the version IDs are different.
Solution Code Example(serialVersionUID)
In this solution, the class has an explicit serialVersionUID that contains a number unique to this version of the class. The JVM will make a good-faith effort to deserialize any serialized object with the same class name and version ID.
class GameWeapon implements Serializable {
  private static final long serialVersionUID = 24L;
 
  int numOfWeapons = 10;
         
  public String toString() {
    return String.valueOf(numOfWeapons);
  }
}
Read more detail about the usage of serialVersionUID in Serialization.

2.2 Do not deviate from the proper signatures of serialization methods

Classes that require special handling during object serialization and deserialization must implement special methods with exactly the following signatures:
private void writeObject(java.io.ObjectOutputStream out)
    throws IOException;
private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;
private void readObjectNoData()
    throws ObjectStreamException;

2.3 Do not serialize unencrypted sensitive data

Although serialization allows an object's state to be saved as a sequence of bytes and then reconstituted at a later time, it provides no mechanism to protect the serialized data. An attacker who gains access to the serialized data can use it to discover sensitive information and to determine implementation details of the objects. An attacker can also modify the serialized data in an attempt to compromise the system when the malicious data is deserialized.
Examples of sensitive data that should never be serialized include cryptographic keys, digital certificates, and classes that may hold references to sensitive data at the time of serialization.
Problematic Code Example
The data members of class Point are private. Assuming the coordinates are sensitive, their presence in the data stream would expose them to malicious tampering.
public class Point implements Serializable {
  private double x;
  private double y;
 
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }
 
  public Point() {
    // No-argument constructor
  }
}
 
public class Coordinates extends Point {
  public static void main(String[] args) {
    FileOutputStream fout = null;
    try {
      Point p = new Point(5, 2);
      fout = new FileOutputStream("point.ser");
      ObjectOutputStream oout = new ObjectOutputStream(fout);
      oout.writeObject(p);
    } catch (Throwable t) {
      // Forward to handler
    } finally {
      if (fout != null) {
        try {
          fout.close();
        } catch (IOException x) {
          // Handle error
        }
      }
    }
  }
}
The above approach is inappropriate for any class that contains sensitive data.
Solution Code Example
Make the sensitive data fields as transient so that serialization does not apply to transient fields.
public class Point implements Serializable {
 private transient double x; // Declared transient
 private transient double y; // Declared transient
 
 public Point(double x, double y) {
  this.x = x;
  this.y = y;
 }
 
 public Point() {
   // No-argument constructor
 }
}
 
public class Coordinates extends Point {
  public static void main(String[] args) {
    FileOutputStream fout = null;
    try {
      Point p = new Point(5,2);
      fout = new FileOutputStream("point.ser");
      ObjectOutputStream oout = new ObjectOutputStream(fout);
      oout.writeObject(p);
      oout.close();
    } catch (Exception e) {
      // Forward to handler
    } finally {
      if (fout != null) {
        try {
          fout.close();
        } catch (IOException x) {
          // Handle error
        }
      }
    }
  }
}

2.4 Do not serialize instances of inner classes

  1. An inner class is a nested class that is not explicitly or implicitly declared static. Serialization of inner classes (including local and anonymous classes) is error prone. Do not serialize instances of inner classes.
  2. Serialization of static member classes is permitted.
Problematic Code Example
In this problematic code example, the fields contained within the outer class are serialized when the inner class is serialized:
public class OuterSer implements Serializable {
  private int rank;
  class InnerSer implements Serializable {
    protected String name;
    // ...
  }
}
Solution Code Example 1
The InnerSer class of this compliant solution deliberately fails to implement the Serializable interface:
public class OuterSer implements Serializable {
  private int rank;
  class InnerSer {
    protected String name;
    // ...
  }
}
Solution Code Example 2
If an inner and outer class must both be Serializable, the inner class can be declared static to prevent a serialized inner class from also serializing its outer class.
public class OuterSer implements Serializable {
  private int rank;
  static class InnerSer implements Serializable {
    protected String name;
    // ...
  }
}

2.5 Do not invoke overridable methods from the readObject() method

The readObject() method must not call any overridable methods. Invoking overridable methods from the readObject() method can provide the overriding method with access to the object's state before it is fully initialized. This premature access is possible because, in deserialization, readObject plays the role of object constructor and therefore object initialization is not complete until readObject exits.
Problematic Code Example
This noncompliant code example invokes an overridable method from the readObject() method:
private void readObject(final ObjectInputStream stream)
                        throws IOException, ClassNotFoundException {
  overridableMethod();
  stream.defaultReadObject();
}
 
public void overridableMethod() {
  // ...
}
Solution Code Example
This compliant solution removes the call to the overridable method. When removing such calls is infeasible, declare the method private or final.
private void readObject(final ObjectInputStream stream)
                        throws IOException, ClassNotFoundException {
  stream.defaultReadObject();
}

2.6 Avoid memory and resource leaks during serialization

Serialization can extend the lifetime of objects, preventing their garbage collection. The ObjectOutputStream ensures that each object is written to the stream only once by retaining a reference (or handle) to each object written to the stream.
Problematic Code Example
This problematic code example reads and serializes data from an external sensor. Each invocation of the readSensorData() method returns a newly created SensorData instance, each containing one megabyte of data. SensorData instances are pure data streams, containing data and arrays but lacking references to other SensorData objects. An OutOfMemoryError can occur because the stream remains open while new objects are being written to it.
class SensorData implements Serializable {
  // 1 MB of data per instance!
  ...
  public static SensorData readSensorData() {...}
  public static boolean isAvailable() {...}
}
 
class SerializeSensorData {
  public static void main(String[] args) throws IOException {
    ObjectOutputStream out = null;
    try {
      out = new ObjectOutputStream(
          new BufferedOutputStream(new FileOutputStream("ser.dat")));
      while (SensorData.isAvailable()) {
        // Note that each SensorData object is 1 MB in size
        SensorData sd = SensorData.readSensorData();
        out.writeObject(sd);
      }
    } finally {
      if (out != null) {
        out.close();
      }
    }
  }
}
Solution Code Example
This solution takes advantage of the known properties of the sensor data by resetting the output stream after each writes. The reset clears the output stream's internal object cache; consequently, the cache no longer maintains references to previously written SensorData objects. The garbage collector can collect SensorData instances that are no longer needed.
class SerializeSensorData {
  public static void main(String[] args) throws IOException {
    ObjectOutputStream out = null;
    try {
      out = new ObjectOutputStream(
          new BufferedOutputStream(new FileOutputStream("ser.dat")));
      while (SensorData.isAvailable()) {
        // Note that each SensorData object is 1 MB in size
        SensorData sd = SensorData.readSensorData();
        out.writeObject(sd);
        out.reset(); // Reset the stream
      }
    } finally {
      if (out != null) {
        out.close();
      }
    }
  }
}

2.7 Prevent overwriting of externalizable objects

Classes that implement the Externalizable interface must provide the readExternal() and writeExternal() methods. These methods have package-private or public access, and so they can be called by trusted and untrusted code alike. Consequently, programs must ensure that these methods execute only when intended and that they cannot overwrite the internal state of objects at arbitrary points during program execution.
Problematic Code Example
This problematic code example allows any caller to reset the value of the object at any time because the readExternal() method is necessarily declared to be public and lacks protection against hostile callers:
public void readExternal(ObjectInput in)
                         throws IOException, ClassNotFoundException {
   // Read instance fields
   this.name = (String) in.readObject();
   this.UID = in.readInt();
   // ...
}
Solution Code Example
This solution code example protects against multiple initializations through the use of a Boolean flag that is set after the instance fields have been populated. It also protects against race conditions by synchronizing on a private lock object.
private final Object lock = new Object();
private boolean initialized = false;
 
public void readExternal(ObjectInput in)
                         throws IOException, ClassNotFoundException {
  synchronized (lock) {
    if (!initialized) {
      // Read instance fields
      this.name = (String) in.readObject();
      this.UID = in.readInt();
      // ... 
      initialized = true;
    } else {
      throw new IllegalStateException();
    }
  }
}

2.8 Prevent deserialization of untrusted data

Deserializing untrusted data can cause Java to create an object of an arbitrary attacker-specified class, provided that the class is available on the classpath specified for the JVM. 
You should provide parameterless constructors on non-serialized classes designed for inheritance, and if it needs to implement serializable. When an instance of a class is de-serialized, their constructor is not run and is the same for all serializable superclasses. However, for a non serializable superclass, the super class’s non-parameterize (or default) constructor is called, and if not available, an InvalidClassException will be thrown. 
The readObject() method must check the validity of input stream data and make defensive copies of the instance reference fields if required, similar to constructors. However, the fields should not be final as you need to change their values.

2.9 Declare an explicit serial version UID in every serialization class you write

A new version of a class will be compatible to the older one only if they have the same serial version UID number. By running the servialver utility on a class version, we can get the automatically generated value of serial version UID for that class.

3. Conclusion

In this post, we have seen few basic coding standard rules that can follow while using Serialization in Java Projects.
We have also seen each rule with problematic and solution code examples.

4. Related Posts

Free Spring Boot Tutorial | Full In-depth Course | Learn Spring Boot in 10 Hours


Watch this course on YouTube at Spring Boot Tutorial | Fee 10 Hours Full Course

Comments