Skip to main content

Insecure deserialization

Need

Secure deserialization process

Context

  • Usage of Scala for building scalable and high-performance applications
  • Usage of play.api.mvc for handling HTTP requests and responses in Play Framework
  • Usage of global execution context for Scala concurrent operations

Description

Non compliant code

import java.io.{ByteArrayInputStream, ObjectInputStream}
import play.api.mvc._
import scala.concurrent.ExecutionContext.Implicits.global

class HomeController extends Controller {

def deserializeAction = Action.async(parse.byteArray) { request =>
val bytes = request.body
val ois = new ObjectInputStream(new ByteArrayInputStream(bytes))
val obj = ois.readObject()
Future.successful(Ok("Deserialized object: " + obj.toString))
}
}

The above Scala code represents a Play Framework controller with a single action deserializeAction. This action handles POST requests and tries to deserialize the request body into an object.

The deserialization process happens in the following lines:

val bytes = request.body
val ois = new ObjectInputStream(new ByteArrayInputStream(bytes))
val obj = ois.readObject()

The request.body is read as a byte array, then these bytes are converted to an ObjectInputStream via a ByteArrayInputStream. Finally, the readObject method is called to deserialize the object.

The vulnerability lies in the fact that the readObject method is called without any validation of the incoming data nor casting to a specific type. This can lead to an insecure deserialization attack where an attacker can craft a serialized object with malicious intent (e.g., to execute arbitrary code, to bypass authentication, etc.).

It's also important to note that this code does not handle exceptions that might be thrown during the deserialization process, which could lead to denial-of-service attacks if an attacker sends data that causes an exception to be thrown.

The code should be fixed by adding validation of the incoming data and by using a safe method for deserialization that includes type checking.

Steps

  • Validate the deserialized object before using it
  • Cast the deserialized object to a specific type
  • Implement a whitelist or blacklist approach to restrict the types of objects that can be deserialized
  • Consider using a safer alternative to Java serialization, such as JSON or XML serialization

Compliant code

import java.io.{ByteArrayInputStream, ObjectInputStream}
import play.api.mvc._
import scala.concurrent.ExecutionContext.Implicits.global

class HomeController extends Controller {

def deserializeAction = Action.async(parse.byteArray) { request =>
val bytes = request.body
val ois = new ObjectInputStream(new ByteArrayInputStream(bytes))
val obj = ois.readObject()

obj match {
case obj: ExpectedType => // Replace ExpectedType with the type you expect
Future.successful(Ok("Deserialized object: " + obj.toString))
case _ =>
Future.successful(BadRequest("Invalid object type"))
}
}
}

The original code was vulnerable to insecure deserialization attacks because it did not validate the type of the deserialized object before using it. This could allow an attacker to inject malicious objects into the application, leading to a variety of potential attacks such as remote code execution, denial of service, or privilege escalation.

The fixed code mitigates this vulnerability by checking the type of the deserialized object before using it. If the object is of the expected type, it is used normally. If it is not, a BadRequest response is returned, indicating that the request contained an invalid object type.

This is a simple type check and may not be sufficient for all use cases. For more complex scenarios, you might need to implement a whitelist or blacklist of allowed or disallowed types, or perform more detailed validation of the object's content.

Additionally, consider using a safer alternative to Java serialization, such as JSON or XML serialization. These formats are less prone to deserialization vulnerabilities and are often easier to validate and debug.

References