Link Search Menu Expand Document

Unsafe Deserialization in Java

Java implements serialization natively for objects that implement the Serializable interface via the ObjectInputStream and ObjectOutputStream facilities. The binary format used directly references classes by name that are eventually loaded dynamically, provided that they are in the class path. This potentially allows instantiating objects of classes not initially intended by the developer, thus it is very important that untrusted data is not deserialized as is.

Developers may customize some aspects of the serialization process by providing callbacks such as writeReplace and readResolve. This could be exploited by an attacker to build chains by building complex objects that eventually lead to code execution or other actions on the target. Especially when complex and well-known libraries and frameworks are used, attackers may leverage publicly available tools such as ysoserial to easily craft the appropriate payload.

Vulnerable Example

The following Spring controller uses the data coming from the client request to deserialize an object:

@Controller
public class MyController {
    @RequestMapping(value = "/", method = GET)
    public String index(@CookieValue(value = "myCookie") String myCookieString) {
        // decode the Base64 cookie value
        byte[] myCookieBytes = Base64.getDecoder().decode(myCookieString);

        // use those bytes to deserialize an object
        ByteArrayInputStream buffer = new ByteArrayInputStream(myCookieBytes);
        try (ObjectInputStream stream = new ObjectInputStream(buffer)) {
            MyObject myObject = stream.readObject();

            // ...
        }
    }
}

Prevention

Never pass user-supplied input to the Java deserialization mechanism, and opt for data-only serialization formats such as JSON.

If the deserialization of untrusted data is really necessary, consider adopting an allow list approach to only allow objects of certain classes to be deserialized.

Since Java version 9, it has been possible to specify a deserialization filter in several ways. One example is to use the setObjectInputFilter method for ObjectInputStream objects before their use. The setObjectInputFilter method takes, as an argument, a method that implements the filtering logic. The following filter only allows one to deserialize instances of the MyClass class:

ObjectInputStream objectInputStream = new ObjectInputStream(buffer);
stream.setObjectInputFilter(MyFilter::myFilter);

Where:

public class MyFilter {
    static ObjectInputFilter.Status myFilter(ObjectInputFilter.FilterInfo info) {
        Class<?> serialClass = info.serialClass();
        if (serialClass != null) {
            return serialClass.getName().equals(MyClass.class.getName())
                    ? ObjectInputFilter.Status.ALLOWED
                    : ObjectInputFilter.Status.REJECTED;
        }
        return ObjectInputFilter.Status.UNDECIDED;
    }
}

Alternatively, it is possible to implement a similar solution by specializing the implementation of the ObjectInputStream object. The following snippet only allows one to deserialize instances of the MyClass class:

public class MyFilteringInputStream extends ObjectInputStream {
    public MyFilteringInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {
        if (!objectStreamClass.getName().equals(MyClass.class.getName())) {
            throw new InvalidClassException("Forbidden class", objectStreamClass.getName());
        }
        return super.resolveClass(objectStreamClass);
    }
}

It is then possible to invoke the deserialization in the usual way:

ObjectInputStream objectInputStream = new MyFilteringInputStream(buffer);
objectInputStream.readObject();

References

OWASP - Deserialization Cheat Sheet Wikipedia - Serialization Oracle - Serializable Oracle - Serialization Filtering GitHub - Ysoserial