Skip to end of metadata
Go to start of metadata

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. This can be troublesome from a security perspective; not only does it promote dead code, it also forces the provider to maintain a compatible codebase for the lifetime of their products.

Classes that implement Serializable without overriding its functionality are said to be using the default serialized form. In the event the class changes, byte streams produced by users of old versions of the class become incompatible with the new implementation. Consequently, serializable classes that rely on the default serialized form cannot be evolved without compromising compatibility.

To enable compatible evolution of a serializable class, developers must use a custom serialized form, which is more flexible than a default form. Specifically,

  • Use of a custom form severs the dependence of the stream format on the code of the implementing class.
  • The code generated for deserializing a custom form can handle compatible deviations from the serialized form, like extra fields.

As a result, developers need neither maintain the earlier version of the code nor explicitly support the original serialized form.

Note that compliance with this rule, while necessary, is not sufficient to guarantee compatible evolution of serializable classes. For a full discussion of compatible evolution of serializable classes, see the Java Object Serialization Specification (version 6), Chapter 5, "Versioning of Serializable Objects" [Sun 2006].

Noncompliant Code Example

This noncompliant 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.

Compliant Solution (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);
  }
}

Compliant Solution (serialPersistentFields)

Ideally, Serializable should be implemented only for stable classes. One way to maintain the original serialized form and allow the class to evolve is to use custom serialization with the help of serialPersistentFields. The static and transient qualifiers specify which fields should not be serialized, whereas the serialPersistentFields field specifies which fields should be serialized. It also relieves the class from defining the serializable field within the class implementation, decoupling the current implementation from the overall logic. New fields can easily be added without breaking compatibility across releases.

class WeaponStore implements Serializable {
  int numOfWeapons = 10; // Total number of weapons
}

public class GameWeapon implements Serializable {
  WeaponStore ws = new WeaponStore();
  private static final ObjectStreamField[] serialPersistentFields
      = {new ObjectStreamField("ws", WeaponStore.class)};

  private void readObject(ObjectInputStream ois)
      throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    this.ws = (WeaponStore) gf.get("ws", ws);
  }
	 
  private void writeObject(ObjectOutputStream oos) throws IOException {
    ObjectOutputStream.PutField pf = oos.putFields();
    pf.put("ws", ws);
    oos.writeFields();
  }
	 
  public String toString() {
    return String.valueOf(ws);
  }
}

Risk Assessment

Failure to provide a consistent serialization mechanism across releases can limit the extensibility of classes. If classes are extended, compatibility issues may result.

Rule

Severity

Likelihood

Remediation Cost

Priority

Level

SER00-J

Low

Probable

High

P2

L3

Automated Detection

Automated detection of classes that use the default serialized form is straightforward.


Related Guidelines

MITRE CWE

CWE-589, Call to Non-ubiquitous API

Bibliography

[API 2014]


[Bloch 2008]

Item 74, "Implement Serialization Judiciously"

[Harold 2006]

Section 13.7.5, "serialPersistentFields"

[Sun 2006]

Java Object Serialization Specification



16 Comments

  1. The requirement that may be normative is to maintain serialization compatibility. The advice of how to do so is clearly a recommendation and is nonnormative in itself. Not clear to me whether this rule should be normative. Thoughts, anyone?

    1. (Summary of a Dean-Dave discussion)

      A guideline need only be normative if it cites a problem that is (a) easily detectable by a human, and (b) indicative of a vulnerability. Solutions need not be normative. Ergo this rule is normative since it cites a normative problem, even if the solution is non-normative.

      A more interesting aspect on this rule is that identifying code as a violation of this rule requires noting that a serializable class has evolved over time (perhaps leaving serialized forms incompatible). This is easily detectable from two snapshots of a project, but not from just one. Which strains our notion of 'easily detectable' but IMHO does not break it.

      1. I've changed the name and intro text of the rule so that it now refers to a single version of a class, rather than a series of versions. I made this change to bring the rule name/intro inline with the code examples.

        Moreover, I feel that, as a general principle, rules which relate to multiple versions of code should be out-of-scope for this standard.

  2. How is the first CS compliant? Including a user-specified ID seems like a necessary, not sufficient, condition for compliance. Future evolutions of the class can still easily break serialization compatibility.

    1. If we have a serialVersionUID associated with an object then atleast JVM will not fail in finding the original serialized form. But If we don't serialize the updated object we will not be seeing the updates in the original serialized form.

      Thoughts?

      1. Having a serial version ID does assume that future maintainers change the ID when the class's internal form changes. Yitzhak is right iff the future maintainers fail to change the ID.

        1. David, I'm not sure I quite understand your point here.  As i understand the rule, and Java serialization in general, the UID is there as a sanity-check, much like a hash for a document. It is not meant for software engineering purposes, but to prevent mismatches between class definitions and serialized forms. So, if future developers manually change the UID w/o changing the name of the class they have will directly violate this rule, even if they change nothing else in the class.  Java does not provide for a single class gracefully handling multiple UIDs. That is, the mapping is strictly one-to-one. Instead, if a developer wishes to design an incompatible version of the class, they must define a new class with a different name. Indeed, i think that this stringent requirement is why Bloch cautions against using the Serializable interface.

          Perhaps I have misunderstood?

          1. Afraid so. The UID is a sanity check. This means that the deserializer will assume that a deserialized object is deserializable by a class if and only if their UIDs match. This helps to mitigate the two dangers:

            • A class deserializes an object improperly, perhaps because the class definition has changed (w/o the UID changing), or because the object was maliciously crafted.
            • A class fails to deserialize an object, even though the object is deserializable. This can happen because the UID changes w/o the class changing. If a class doesn't assign its own UID, the JVM creates one using some sort of hashing function . This means that if the class undergoes a trivial change (that doesn't affect deserialization), the UID created will change, and the class will fail to deserialize objects created during its earlier life.

            So to make an 'incompatible' change of a class, eg one that prevents deserialization of earlier objects, the maintainer MUST change the UID. To make a compatible change, the maintainer MUST NOT change the UID...which requires having a UID manually defined in the first place.

            1. Sorry, I'm only more confused now.  You start by saying "afraid so" but then your reply seems in exact agreement with what I wrote. So, what exactly have i misunderstood?

              Also, I'm missing something in your final comment. If i understand Java correctly, you can make an incompatible change to a class without changing the UID. Simply maintain the name and UID and then switch around the fields. The JVM will match the class name and UID and proceed to attempt the serialization, which will then fail because of mismatched contents.  Are we using the term "incompatible" differently?

              1. Yitzhak, by 'incompatible' I meant a class that wouldn't properly deserialize objects made during its old version.

                That is, the mapping is strictly one-to-one. Instead, if a developer wishes to design an incompatible version of the class, they must define a new class with a different name.

                This is the part of your earlier comment I disagree with. If a dev wishes to design an incompatible version of his class, he merely needs to change the UID.

                I agree completely with your second paragraph.

                1. Excellent, i see the confusion now. My sentence was ambiguous and i totally missed the second reading (now obvious).  By "must" I meant normatively: i.e. they ought to do so to conform with this rule.  Clearly, though, as you say, they certainly can break things trivially by changing the UID.

                  So now back to your original comment. You say:

                  Yitzhak is right iff the future maintainers fail to change the ID.

                  I read the "iff" as "if-and-only-if", hence my starting on this whole thread. Was that your intent? If not, may I suggest that you edit your comment, and then we remove this whole exchange so as not to confuse future readers? (That said, I think we should merge some of this explanatory material into the main rule once we've gone to the effort. I volunteer)

                  1. Yes, I meant 'if-and-only-if'. And we do clearly need to make the rule clearer on this point.

  3. (readObject should throw ClassNotFoundException, not catch it. The cop out /* Forward to handler */ is nonsense - the object is in the process of construction, it doesn't have a handler.)

    1. Made the change you suggested. Actually the 'foward to handler' doesn't refer to a handler of the object, but rather a global error handler for the entire application. Still that line added nothing and was rather confusing; hence I removed it.

  4. Any thoughts on adding an exception for classes deemed "stable" by the developers? Or, is the idea that we're forbidding reliance on the default form, period?

  5. Automated Detection:

    Sonar:

    pmd:MissingSerialVersionUID