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. Some classes have triggers that execute additional code when they are created in this manner; see SEC58-J. Deserialization methods should not perform potentially dangerous operations for more information. If such classes are poorly designed, such code could even invoke arbitrary methods, such as Runtime.exec()
with an attacker-supplied argument. Therefore, untrusted input to be deserialized should be first validated to ensure that the serialized data contains only trusted classes, perhaps specified in a whitelist of trusted classes. This can be done by overriding the resolveClass()
method of the java.io.ObjectInputStream
class.
This rule applies only to untrusted data. Data that does not cross a program's trust boundary is, by definition, trusted and can be deserialized without violating this rule. See the exception SER12-EX0 for more information.
Deserialization of data that is trusted but must cross a trust boundary (perhaps because it originates from a different host) automatically complies with this rule, but must also comply with SER02-J. Sign then seal objects before sending them outside a trust boundary for more information.
Non-Compliant Code Example
This non-compliant code deserializes a byte array without first validating what classes will be created. If the object being deserialized belongs to a class with a lax readObject()
method, this could result in remote code execution.
import java.io.*; class DeserializeExample { public static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException { Object ret = null; try (ByteArrayInputStream bais = new ByteArrayInputStream(buffer)) { try (ObjectInputStream ois = new ObjectInputStream(bais)) { ret = ois.readObject(); } } return ret; } }
Compliant Solution (Look-Ahead Java Deserialization)
This compliant solution is based on http://www.ibm.com/developerworks/library/se-lookahead/. It inspects the class of any object being deserialized, before its readObject()
method is invoked. The code consequently throws an InvalidClassException
unless the class of the object (and of all sub-objects) is either a GoodClass1
or a GoodClass2
.
import java.io.*; import java.util.*; class WhitelistedObjectInputStream extends ObjectInputStream { public Set whitelist; public WhitelistedObjectInputStream(InputStream inputStream, Set wl) throws IOException { super(inputStream); whitelist = wl; } @Override protected Class<?> resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException { if (!whitelist.contains(cls.getName())) { throw new InvalidClassException("Unexpected serialized class", cls.getName()); } return super.resolveClass(cls); } } class DeserializeExample { private static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException { Object ret = null; Set whitelist = new HashSet<String>(Arrays.asList(new String[]{"GoodClass1","GoodClass2"})); try (ByteArrayInputStream bais = new ByteArrayInputStream(buffer)) { try (WhitelistedObjectInputStream ois = new WhitelistedObjectInputStream(bais, whitelist)) { ret = ois.readObject(); } } return ret; } }
It might appear that the above compliant solution violates Rule OBJ09-J. Compare classes and not class names. However, the security issue addressed by that rule is applicable only when comparing the class of an object that might have been loaded by a foreign ClassLoader
, i.e., an object x for which it cannot be guaranteed that x.getClass()==Class.forName(x.getClass().getName())
. In WhitelistedObjectInputStream.resolveClass()
(which is the method that does the comparison of class names), a check could be added to verify that the return value (let's call it "ret
") is such that ret == Class.forName(ret.getName())
, but this check would always succeed, so it is pointless to add.
On a somewhat related point, it may be noted that ObjectInputStream.resolveClass()
compares the serialVersionUID
from the serialized data to the serialVersionUID
of the Class
object that it is going to return; if there is a mismatch, it throws an exception.
Whitelist
The construction of a suitable whitelist is itself a real challenge. Remote-code-execution exploits have been constructed, not only for classes in the Apache Commons Collection, but also for such core classes as java.util.concurrent.AtomicReferenceArray
(CVE 2012-0507), and even simple classes like java.util.HashSet
[Terse 2015] and java.net.URL
[Terse 2015] can cause a JVM to hang or exhaust memory.
Exceptions
SER12-EX0: Serialized data from a trusted input source does not require sanitization, provided that the code clearly documents that it relies on the input source being trustworthy. For example, if a library is being audited, a routine of that library may have a documented precondition that its callers pre-sanitize any passed-in serialized data or confirm the input source as trustworthy.
Related Vulnerabilities
CERT Vulnerability #576313 describes a family of exploitable vulnerabilities that arise from violating this rule.
Risk Assessment
Whether a violation of this rule is exploitable depends on what classes are on the JVM's classpath. (Note that this is a property of the execution environment, not of the code being audited.) In the worst case, it could lead to remote execution of arbitrary code.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
SER12-J | High | Likely | High | P9 | L2 |
Automated Detection
Tool | Version | Checker | Description |
---|---|---|---|
Useful for developing exploits that detect violation of this rule |
It should not be difficult to write a static analysis to check for deserialization that fails to override resolveClass()
to compare against a whitelist.
Related Guidelines
Bibliography
[API 2014] | |
| |
| |
[Terse 2015] | Terse Systems, Closing the Open Door of Java Object Serialization, Nov 8, 2015 |