Serializing/deserialzing gacContainer (typical Java class)
The gacContainer object is an instance of MelonContainer which is a plain Java class.
MelonContainer gacContainer = new MelonContainer(
LocalDate.now().plusDays(15), “ML9000SQA0”,
new Melon(“Gac”, 5000));
After serializing it in a file called object.data we obtain the byte stream representing the gacContainer state. While you can inspect this file in the bundled code (use a hex-editor such as https://hexed.it/), here is a human-readable interpretation of its content:
Figure 4.3 – Human-readable interpretation of MelonContainer serialization
The deserialization operation takes place by building the object-graph from the top-down. When the class name is known the compiler creates an object by calling the non-arguments constructor of the first superclass of MelonContainer that is non-serializable. In this case, that is the non-argument constructor of java.lang.Object. So, the compiler is not calling the constructor of MelonContainer.Next, the fields are created and set to the default values, so the created object has expiration, batch, and melon as null. Of course, this is not the correct state of our object, so we continue processing the serialization stream to extract and populate the fields with the correct values. This can be seen in the following diagram (on left-side, the created object has default values; right-side, the fields have been populated with the correct state):
Figure 4.4 – Populating the created object with the correct state
When the compiler hits the melon field it must perform the same steps to obtain the Melon instance. It sets the fields (type and weight to null, respectively, 0.0f). Further, it reads the real values from the stream and sets the correct state for the melon object.Finally, after the entire stream is read, the compiler will link the objects accordingly. This is shown in the following figure (1, 2, and 3 represent the steps of the deserialization operation):
Figure 4.5 – Linking the objects to obtain the final state
At this point, the deserialization operation has been done and we can use the resulting object.
Deserializing malicious stream
Providing a malicious stream means altering the object state before deserialization. This can be done in many ways. For instance, we can manually modify the object.data in an editor (this is like an untrusted source) as in the following figure where we replaced the valid batch ML9000SQA0 with the invalid batch 0000000000:
Figure 4.6 – Modify the original stream to obtain a malicious stream
If we deserialize the malicious stream (in the bundle code, you can find it as the object_malicious.data file) then you can see that the corrupted data has “successfully” landed in our object (a simple call of toString() reveals that batch is 0000000000):
MelonContainer{expiration=2023-02-26,
batch=0000000000, melon=Melon{type=Gac, weight=5000.0}}
The guarding conditions from Melon/MelonContainer constructors are useless since the deserialization doesn’t call these constructors.So, if we summarize the shortcomings of serializing/deserializing a Java class, we must highlight the presence of the window of time that occurs when the objects are in an improper state (waiting for the compiler to populate fields with correct data and to link them in the final graph), and the risk of dealing with malicious states. Now, let’s pass a Java record through this process.