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);
objectInputStream.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