SER00-G: Do Not Deserialize Untrusted Data
Introduction
Deserializing untrusted data can create serious security vulnerabilities for your application by allowing attackers to create objects of any class that the Java Virtual Machine (JVM) can load. This can lead to risks such as remote code execution and denial-of-service (DoS) attacks. By default, the JVM uses the first non-bootclass class loader it encounters to perform class deserialization. Starting in Java 1.9, the stack now searches for the most recent class loader, which is neither the new platform class loader nor an ancestor due to the module system. Additionally, if remote code invocation (RIM) has been explicitly enabled, an attacker could run code from a remote codebase using this technique.
The safest approach is to avoid deserializing data from untrusted sources altogether. Only when it's absolutely necessary to use deserialization should you use other protection methods like whitelisting.
Deserializing an object from a stream requires creating a new object and populating its fields. If you deserialize untrusted data, the attacker can access the objects being created and their order, allowing them to control the JVM that deserialized the stream. There is a collection of well-known gadgets and presumably unknown vulnerable classes an attacker can use to execute attacker-supplied byte codes and other dangerous exploits.
Serialization supports the transformation of a graph of objects into a stream of bytes for storage or transmission.
These bytes can be transformed back into a graph of objects. Objects, such as the following Bicycle
class need to implement the Serializable
class to be serializable:
uses java.io.Serializable
public class Bicycle implements Serializable {
private static final var serialVersionUID : long = 5754104541168320730L
private var _id: int as id
private var _name : String as name
private var _nbrWheels : int as nbrWheels
public construct(id_0 : int, name_0 : String, nbrWheels_0 : int) {
this.id = id_0
this.name = name_0
this.nbrWheels = nbrWheels_0
}
}
By default, non-transient and non-static fields are serialized. The following class contains methods to serialize and deserialize an object to and from an array of bytes:
uses java.io.*
public class Serial {
public static function serialize(o : Object) : byte[] {
using (var ba = new ByteArrayOutputStream(),
var oos = new ObjectOutputStream(ba)) {
oos.writeObject(o)
return ba.toByteArray()
}
}
public static function deserialize(bytes : byte[]) : Object {
return new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject()
}
}
The serialize
method calls the writeObject
of an ObjectOutputStream
object to serialize the specified root object.
The deserialize
method calls the readObject
of an ObjectInputStream
object to deserialize the serialized bytes.
Deserialization of trusted data is allowed, provided both endpoints are adequately secured.
Non-Compliant Code Example
The following non-compliant code deserializes a byte array without first validating what classes will be created. The program creates and serializes a new Bicycle object and a new File object (an exploit gadget in this example).
// Noncompliant Code Example
var serializedBicycle : byte[] = null
var serializedFile : byte[] = null
try {
serializedBicycle = Serial.serialize(new Bicycle(0, "Unicycle", 1))
serializedFile = Serial.serialize(new File("file.txt"))
}
catch (e1 : IOException) {
e1.printStackTrace()
}
var bicycle0 : Bicycle = deserialize(serializedBicycle) as Bicycle
print(bicycle0.name + " has been deserialized.")
try {
var exploit : Object = deserialize(serializedFile)
System.out.println("Exploit has been deserialized.")
}
catch (e : ClassNotFoundException) {
e.printStackTrace()
}
catch (e : IOException) {
e.printStackTrace()
}
The output from executing this program is:
Unicycle has been deserialized.
Exploit has been deserialized.
The program deserializes both the expected object and the exploit without complaint. In this case, the exploit was innocuous but could have easily been a remote code execution.
Compliant Solution (Look-Ahead Object Input Streams)
Look-ahead deserialization or look-ahead object input streams (LAOIS) can be used to create an allow list or deny list of objects. There are several off-the-shelf solutions; the most popular is JEP 290, which is fully integrated into Java 9 and partially supported by earlier releases.
Filtering incoming object-serialization data streams improves security and robustness by narrowing the set of deserializable classes to a context-appropriate set of classes known to be secure.
The first step is to define a custom filter by implementing the checkInput
method of the ObjectInputFilter
interface. The checkInput
method is called during deserialization to accept or reject specific classes, packages, or modules and enforce limits on array sizes, graph depth, total references, and stream size.
The following BikeFilter
class overrides the checkInput
function with one that returns Status.ALLOWED
for the Bicycle class and Status.REJECTED
for any other classes. If the filter cannot determine the status, it should return Status.UNDECIDED
.
// Compliant Solution
uses java.io.ObjectInputFilter
class BikeFilter implements ObjectInputFilter {
override public function checkInput(filterInfo : FilterInfo) : Status {
var clazz : Class<Object> = filterInfo.serialClass()
if (clazz != null) {
if (clazz.Name == "Bicycle") {
return Status.ALLOWED
} else {
return Status.REJECTED
}
}
return Status.UNDECIDED
}
}
We can then add the following function to the Serialize
class:
// Compliant Solution
public static function deserialize(buffer : byte[],
filter : ObjectInputFilter) : Object {
var obj : Object
using (var ois = new ObjectInputStream(new ByteArrayInputStream(buffer)))
{
ois.setObjectInputFilter(filter)
obj = ois.readObject()
}
return obj
}
This function takes an additional ObjectInputFilter
parameter and calls the setObjectInputFilter
method on the ObjectInputStream
to apply the filter to any subsequent calls to readObject
. We can then change the deserialize
function called by our non-compliant code example to apply the BikeFilter:
// Compliant Solution
private static function deserialize(buffer : byte[]) : Object {
var filter : BikeFilter = new BikeFilter()
return Serial.deserialize(buffer, filter)
}
The modified code will still deserialize the Unicycle
object but will reject any object that is not instantiated from the Bicycle
class:
Unicycle has been deserialized.
java.io.InvalidClassException: filter status: REJECTED
at java.base/java.io.ObjectInputStream.filterCheck(ObjectInputStream.java:1287)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1896)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1772)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2060)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
at Serial.deserialize(Serial.gs:20)
Building a suitable allow list can be challenging. Exploits have been constructed for classes in the Apache Commons Collection and core classes such as java.util.concurrent.AtomicReferenceArray
(CVE 2012-0507). Even simple classes, like java.util.HashSet
[Terse 2015] and java.net.URL
[Terse 2015] can cause a denial-of-service attack.
Risk Assessment
Deserializing untrusted data can allow an attacker to execute arbitrary code in the JVM, perform a denial-of-service attack, or perform a variety of other exploits.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
SER00-G | High | Highly Likely | Medium | L18 | L1 |
Additional Resources
- Gosu Secure Coding Guidelines
- CERT Oracle Secure Coding Standard for Java
- MITRE CWE
- Closing the Open Door of Java Object Serialization
- Combating Java Deserialization Vulnerabilities with Look-Ahead Object Input Streams (LAOIS)
- Java SE 11 API Specification
- Classes ObjectInputStream, ObjectInputFilter and ObjectInputFilter
- Secure Coding Rules for Java: Serialization
- JEP 290: Filter Incoming Serialization Data
- CVE 2012-0507
- CERT Vulnerability #576313: Apache Commons Collections Java library insecurely deserializes data
Was this page helpful?