Link Search Menu Expand Document

Unsafe Deserialization in Scala

Scala (the same as 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 Play controller uses the data coming from the client request to deserialize an object:

def handler() =
  AuthAction(parse.multipartFormData) { implicit request => {
    request.body.file("file") match {
      case Some(file) => {
        // deserialize data from Base64 file upload
        val base64Data = new String(Files.readAllBytes(Paths.get(file.ref.path.toString()))).trim()
        val data = Base64.getDecoder().decode(base64Data)
        val ois = new ObjectInputStream(new ByteArrayInputStream(data))
        val object = ois.readObject().asInstanceOf[MyClass]
        ois.close()

        // ...
      }
      case None => InternalServerError("...")
    }
  }
}

Prevention

Never pass user-supplied input to the Scala 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.

It is possible to specialize the implementation of the ObjectInputStream object. The following snippet only allows one to deserialize instances of the MyClass class:

class SafeInputStream(inputStream: InputStream) extends ObjectInputStream(inputStream) {
  override def resolveClass(objectStreamClass: java.io.ObjectStreamClass): Class[_] = {
    objectStreamClass.getName match {
      case "MyClass" | "scala.Some" | "scala.Option" => super.resolveClass(objectStreamClass)
      case _ => throw new InvalidClassException("Forbidden class", objectStreamClass.getName)
    }
  }
}

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

val ois = new SafeInputStream(new ByteArrayInputStream(data))
val object = ois.readObject().asInstanceOf[MyClass]

References

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