Classes that require special handling during object serialization and deserialization must implement special methods with exactly the following signatures [API 2014]:

private void writeObject(java.io.ObjectOutputStream out) 
    throws IOException;
private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;
private void readObjectNoData()
    throws ObjectStreamException;

Note that these methods must be declared private for any serializable class. Serializable classes may also implement the readResolve() and writeReplace() methods.
According to the Serialization Specification [Sun 2006], readResolve() and writeReplace() method documentation:

For Serializable and Externalizable classes, the readResolve method allows a class to replace/resolve the object read from the stream before it is returned to the caller. By implementing the readResolve method, a class can directly control the types and instances of its own instances being deserialized.

For Serializable and Externalizable classes, the writeReplace method allows a class of an object to nominate its own replacement in the stream before the object is written. By implementing the writeReplace method, a class can directly control the types and instances of its own instances being serialized.

It is possible to add any access-specifier to the readResolve() and writeReplace() methods. However, if these methods are declared private, extending classes cannot invoke or override them. Similarly, if these methods are declared static, extending classes cannot override these methods; they can only hide them.

Deviating from these method signatures produces a method that is not invoked during object serialization or deserialization. Such methods, especially if declared public, might be accessible to untrusted code.

Unlike most interfaces, Serializable does not define the method signatures it requires. Interfaces allow only public fields and methods, whereas readObject(), readObjectNoData, and writeObject() must be declared private. Similarly, the Serializable interface does not prevent readResolve() and writeReplace() methods from being declared static, public, or private. Consequently, the Java serialization mechanism fails to let the compiler identify an incorrect method signature for any of these methods.

Noncompliant Code Example (readObject(), writeObject())

This noncompliant code example shows a class Ser with a private constructor, indicating that code external to the class should be unable to create instances of it. The class implements java.io.Serializable and defines public readObject() and writeObject() methods. Consequently, untrusted code can obtain the reconstituted objects by using readObject() and can write to the stream by using writeObject().

public class Ser implements Serializable {
  private final long serialVersionUID = 123456789;
  private Ser() {
    // Initialize
  }
  public static void writeObject(final ObjectOutputStream stream)
    throws IOException {
    stream.defaultWriteObject();
  }	
  public static void readObject(final ObjectInputStream stream)
      throws IOException, ClassNotFoundException {
    stream.defaultReadObject();
  }
}

Note that there are two things wrong with the signatures of writeObject() and readObject() in this Noncompliant Code Example: (1) the method is declared public instead of private, and (2) the method is declared static instead of non-static.  Since the method signatures do not exactly match the required signatures, the JVM will not detect the two methods, resulting in failure to use the custom serialized form.

Compliant Solution (readObject(), writeObject())

This compliant solution declares the readObject() and writeObject() methods private and nonstatic to limit their accessibility:

private void writeObject(final ObjectOutputStream stream)
    throws IOException {
  stream.defaultWriteObject();
}

private void readObject(final ObjectInputStream stream)
    throws IOException, ClassNotFoundException {
  stream.defaultReadObject();
}

Reducing the accessibility also prevents malicious overriding of the two methods.

Noncompliant Code Example (readResolve(), writeReplace())

This noncompliant code example declares the readResolve() and writeReplace() methods as private:

class Extendable implements Serializable {
  private Object readResolve() {
    // ...
  }

  private Object writeReplace() {
    // ...
  }
}

Noncompliant Code Example (readResolve(), writeReplace())

This noncompliant code example declares the readResolve() and writeReplace() methods as static:

class Extendable implements Serializable {
  protected static Object readResolve() {
    // ...
  }

  protected static Object writeReplace() {
    // ...
  }
}

Compliant Solution (readResolve(), writeReplace())

This compliant solution declares the two methods protected while eliminating the static keyword so that subclasses can inherit them:

class Extendable implements Serializable {
  protected Object readResolve() {
    // ...
  }

  protected Object writeReplace() {
    // ...
  }
}

Risk Assessment

Deviating from the proper signatures of serialization methods can lead to unexpected behavior. Failure to limit the accessibility of the readObject() and writeObject() methods can leave code vulnerable to untrusted invocations. Declaring readResolve() and writeReplace() methods to be static or private can force subclasses to silently ignore them, while declaring them public allows them to be invoked by untrusted code.

Rule

Severity

Likelihood

Remediation Cost

Priority

Level

SER01-J

High

Likely

Low

P27

L1

Automated Detection

Tool
Version
Checker
Description
CodeSonar
8.1p0

JAVA.CLASS.SER.ND

Serialization Not Disabled (Java)

Coverity7.5UNSAFE_DESERIALIZATIONImplemented
Parasoft Jtest
2023.1
CERT.SER01.ROWOEnsure that the 'readObject()' and 'writeObject()' methods have the correct signature
PVS-Studio

7.30

V6075
SonarQube
9.9
S2061Custom serialization method signatures should meet requirements

Related Guidelines

MITRE CWE

CWE-502, Deserialization of Untrusted Data

Bibliography

[API 2014]

Class Serializable

[Sun 2006]

Serialization Specification

[Ware 2008]




8 Comments

  1. The NCCE states\

    The accessibility of both the methods is public which allows untrusted code to obtain the reconstituted object (in case of readObject()) and write to the stream (in case of writeObject())

    which really isn't true. ObjectInputStream and ObjectOutputStream store a context while serializing/deserializing and this context is set immediately prior to executing readObject/writeObject methods. Calling defaultReadObject/defaultWriteObject when the context isn't set (which would be the case of a public static readObject/writeObject method being called directly) results in a NotActiveException getting thrown.

    1. An adversary could set up a dummy context to avoid the exception. defaultReadObject still wouldn't alter the object, but there is potential from readFields and also supporting code.

      1. Yes, you can set the context to whatever, but then the defaultReadObject will try to work on the object of your dummy context. readFields works with the class/object of the dummy context as well.

        The point I'm trying to make is that having a public&static readObject/writeObject method doesn't really make any difference. The adversary might as well call stream.defaultReadObject() directly to the same effect.

        On the other hand if the point of the rule is that a public and/or static readObject method will result in default serialized form, I think the NCCE/CS should reflect that, since the way they are at the moment, using .defaultReadObject the default serialized form is used anyway.

        1. Ignoring static for a moment, consider if java.io.File.readObject was public (or protected, as File is unfortunately not final). An adversary could take a File object, set up an ObjectInputStream context and call File.readObject. File.readObject uses readFilelds, so that gives a mutable File for use in TOCTOU attacks or similar.

          1. Ahh, ok, got it now Thomas. Thanks for your patience. That indeed would be a vulnerability.

            I was looking at it from a wrong angle, partly because of the static.

            What about static though? Maybe I'm missing something more, but I can't see static making much of a difference.

  2. There is a fundamental flaw here. If the writeObject or readObject methods are static, or if they are protected, package, or public, then they are invoked by the serializer. They are only invoked when private and nonstatic.

    Offhand, I think this completely invalidates this rule. OTOH the compiler won't holler if you get this wrong...your improper writeObject method just won't get called during serialization. So having these methods declared wrongly may be worthwhile after all.

  3. Does any one know any good sample code for this type of attack (improper signature of writeObject and readObject)

    any help will be appreciated.

    1. I wouldn't call it an attack...it is less related to security than appears. A wrongly-declared method will cause the serializer to not invoke it, which makes the program behave differently than expected.   An attacker may be able to exploit a program with this bug, but further details of how serialization is done in the program would be necessary...see the other serialization rules for more details.