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

LikelyHighP9L2

Automated Detection

Tool
Version
Checker
Description
CodeSonar
8.1p0

JAVA.CLASS.SER.ND

Serialization Not Disabled (Java)

Parasoft Jtest
2023.1
CERT.SER12.VOBDValidate objects before deserialization

ysoserial



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

MITRE CWE

CWE-502, Deserialization of Untrusted Data

Bibliography


  


18 Comments

  1. Needs a compliant solution about using the SecurityManager to prevent sensitive operations during deserialization of untrusted classes.

    1. Also needs exception when either: (1) classpath is trusted (no untrusted class) or (2) deserialization cannot load untrusted data. (Prob should be two exceptions).  And we need some text in front of the code samples (smile)

      And a bibliography

      1. Still need an exception for when classpath is trusted.
        Also the compliant solution needs to comply with OBJ09-J. Compare classes and not class names

  2. > As an alternative to validation of the serialized data, a java.lang.SecurityManager can be used to perform deserialization in a less-privileged context

    Current published exploits focus mostly on code execution during serialization, because that's the most universal reliable way to exploit it.
    But code execution can also be triggered later, when the deserialized objects are being used. Wrapping just the deserialization in a less-privileged context will block some exploits, but it does not block these attacks in general.

    1. I'm curious if you have proof-of-concept code for this, that doesn't violate another CERT rule?

      For such an exploit to work you have to deserialize malicious code that is executed outside of the deserialization process. And your code has to have a framework that executes code based on the (untrusted) deserialized data, and you have to pass your untrusted data to that engine unsanitized. Which will likely violate another CERT rule.

      An example would be if you deserialize data that contains a string to pass to your database engine. An attacker embedes an SQL injection which is correctly deserialized but the string contents are not sanitized before going to your database. This violates IDS00-J. Prevent SQL injection.

      1. I'm not sure if this is addressing your question, but recent PoCs execute during deserialization just because it's more reliable and generic/portable that way, but, as Wouter pointed out, variants could defer execution until after deserialization by producing an object of the type expected by the calling code and only execute the gadget chain after the calling code invokes a method on the returned object.

        As an (untested) example, the ysoserial CommonsCollections1 payload could be modified to omit the outer AnnotationInvocationHandler instance and just return the inner AnnotationInvocationHandler proxy instance (and the rest of the gadget chain it contains) that implements an interface expected by the calling code (i.e. java.util.List), and when that code finally invokes a method on the proxy instance (i.e. List.get(int)), the gadget chain would be executed.

        List<SomeType> myList = (List<SomeType>) ois.readObject(); // deserialize gadget chain without executing, List type arg doesn't matter
        SomeType someObj = myList.get(0); // kick-off gadget chain via proxy
        1. Interesting.

          If I was using whitelisting to make sure only objects of trusted classes got deserialized, I would definitely leave org.apache.commons.collections.map.LazyMap off of that whitelist! :)

          If I am deserializing an object using a SecurityManager as chaparone, then the SecurityManager would (theoretically) guarantee that the deserialization did nothing malicious. This wouldn't stop your hypothetical LazyMap object from doing something nefarious later. But it would give me an opportunity after deserialization to sanitize your object. So I can make sure your object is not a LazyMap, for instance.

          We do have rules for handling potentially malicious objects, such as MET06-J. Do not invoke overridable methods in clone().

      2. A different approach is to run the payload in the finalize method. An example of this can be found in  javax.media.jai.remote.SerializableRenderedImage which contains a finalize() method that ends up calling closeClient() which opens a socket to an attacker controller server that can return a payload to be deserialized at that moment so way after the adhoc securitymanager has been uninstalled

        1. Yet another class to keep off my deserialization whitelist (smile)

          1. Also bear in mind that ad-hoc security managers (installed with the purpose of sandboxing the deserialization process) wont be very effective since they need to allow the runtime permission to change the security manager (so it can be uninstalled or restored after deserialization completes). If thats the case, nothing prevents the attacker to uninstall it from his payload.

            1. I suspect any security-manager-based solution will be more complex than initially planned...security managers tend to be complex. But I do believe a solution is possible...remember the objects you deserialize are all of code in your classpath. If your code never disables a SecurityManager, except in the same object & method where it added it, then you can effectively prevent premature disabling of your security manager.

              This also assumes that you actually do want to disable the security manager after deserialization...a truly secure program would continue with the security manager operational. Sort of like permanently dropping setuid privileges in Unix.

              1. > As an alternative to validation of the serialized data, a java.lang.SecurityManager can be used to perform deserialization in a less-privileged context.

                That sentence reads (at least to me that Im not native speaker) as if an AdHoc security manager is used to sandbox the deserialization process.

                I agree that a truly secure program should not allow changing securityManagers, although sometimes you want to prevent operations during deserialization that need to be allowed during normal execution of the application. Using a system-level SecurityManager is ok because it wont have the change SecurityManager permission enabled (hopefully). If you use an AdHoc one, meaning that you want to apply a different set of policies for the deserialization operation, you will need to uninstall the installed one and install a new one which will require the permission to change SecurityManagers. If you can do that, the payload will be able to do it as well. Even if the gadgets need to be in the classpath, attackers will use dynamic code injection (eg: TemplatesImpl, JNDI, etc) and the injected code will be able to call System.setSecurityManager(null) as part of the payload.

                1. I don't know of Java code that changes security managers mid-program, other than toy demo examples or malware. Most 'real' code has a SM applied to them externally (because they are applets, for example). The security architecture allows you to use your own SM, but I don't know any code that does this....everyone either uses the default SM or none.

                  The default SM decides what privileges to grant code based on its class loader. But malicious deserialized objects use the same classes loaded by the application. So the SM can't discriminate 'malicious' code from application code. Which means either malicious code can disable the SM or benign code can't.

                  Of course, you could program deserialization to be a privilege managed by the default SM (it isn't today). This *could* mitigate the problem if we got all the details right. But part of the problem is classes that allow embedded malicious code such as org.apache.commons.collections.map.LazyMap. So anything less than enforcing serialization privileges in the Java language itself would be insufficient. Such a proposal would also break any code that uses serialization and a SM.

                  You could also write your own custom SM. But as that route is unexplored (and I don't see how a SM could enforce constraints on deserialization without the cooperation of all classes), I shan't suggest it.

                  So I took that statement out of the intro. I now think the SecurityManager would not be helpful in preventing malicious deserialization, and fixing it would require incompatible changes to the language itself.

  3. It may also be worth mentioning that not deserializing any untrusted data at all is the only solutions that can protect you against billion-laughs-style denial-of-service attacks (https://gist.github.com/coekie/a27cc406fc9f3dc7a70d).
    Well, unless you limit your whitelist to the extreme, like not including any classes that can represent any kind of tree structure with a hashCode() implementation; but I don't think that is a realistic approach for most real-world uses of serialization.

  4. There is another solution to the Deserialization problem that requires no profiling, no tuning, no whitelists/blacklists and no code changes.

    https://dzone.com/articles/why-runtime-compartmentalization-is-the-most-compr

    It can mitigate all ysoserial attacks, DoS attacks such as the serialDOS, golden-gadgets, as well as zero-day gadget chains.

    1. The solution you cite is RASP by Waratek. It sounds like a new-and-improved SecurityManager, it seems to provide some sandboxing. However, it addresses the problems at too high a level: how to securely manage a web application that may depend on insecure deserialization. It does not address how to perform deserialization correctly; that is, correctly deserializing non-malicious objects while preventing deserialization of malicious objects. Given the myriad types of malicious objects (from Coekert's DOS attack to malicious finalizer methods), this is a particularly hard problem. Which makes the halting problem appear simple by comparison.

      1. David, this is exactly what it does. It allows deserializing non-malicious objects while preventing deserialization of malicious objects, with no profiling or white/blacklisting. It is in fact a particularly hard problem to solve, but we believe that this approach achieves the best results in the industry right now.