Symmetric denial of service - Scala

Symmetric denial of service - Scala

Need

Prevention of service outages caused by repetitive resource-intensive requests

Context

  • Usage of Scala for functional and object-oriented programming in the JVM environment
  • Usage of play.api.mvc for handling HTTP requests and responses in Play Framework

Description

Non compliant code

        import play.api.mvc._

object VulnerableController extends Controller {
  def vulnerableAction = Action { request =>
    val expensiveOperation = request.body.asJson.get("data").as[String]
    // An expensive operation that consumes a lot of resources or takes too long to be processed
    Thread.sleep(10000) // Simulating an expensive operation
    Ok("Done")
  }
}
        
        

This code illustrates a controller action in Play framework that is prone to a symmetric Denial of Service (DoS) attack. The vulnerableAction method takes in a request and performs an expensive operation that consumes a lot of resources or takes a considerable amount of time to process.

The Thread.sleep(10000) line is used to simulate this expensive operation. It causes the current thread to suspend execution for a specified period. This is just a placeholder for a potentially resource-intensive operation like complex calculations, large database queries, etc.

The problem here is that if an attacker sends multiple requests to this endpoint in quick succession, it can lead to the server becoming unresponsive. This is because each request will cause the server to pause for a significant amount of time (10 seconds in this case), which can quickly exhaust the server's resources if many such requests are made concurrently.

This is a typical example of a symmetric DoS vulnerability, where an attacker can cause a denial of service by repeatedly triggering a resource-intensive operation.

Steps

  • Implement rate limiting to restrict the number of requests that can be made within a certain time frame.
  • Use asynchronous processing to avoid blocking the server thread while waiting for the expensive operation to complete.
  • Implement timeouts for requests to limit the maximum time a request can take to be processed.
  • Optimize the expensive operation to reduce resource consumption and processing time.
  • Consider offloading the expensive operation to a separate service or background task to avoid impacting the responsiveness of the server.

Compliant code

        import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration._

object FixedController extends Controller {
  implicit val ec: ExecutionContext = ExecutionContext.global

  // Implement rate limiting
  val rateLimiter = new RateLimiter(5, 1.minute) // Allow 5 requests per minute

  def fixedAction = Action.async { request =>
    rateLimiter.attempt() match {
      case true => // If the rate limit has not been exceeded
        val expensiveOperation = request.body.asJson.get("data").as[String]
        // Use asynchronous processing and implement a timeout
        val futureResult = Future {
          Thread.sleep(10000) // Simulating an expensive operation
          "Done"
        }
        futureResult.map(result => Ok(result)).recover {
          case _: java.util.concurrent.TimeoutException => RequestTimeout("Request took too long.")
        }
      case false => // If the rate limit has been exceeded
        Future.successful(TooManyRequest("Too many requests. Please try again later."))
    }
  }
}
        
        

The fixed code introduces several measures to mitigate the Symmetric Denial of Service vulnerability:

1. Rate Limiting: A RateLimiter class is used to limit the number of requests that can be made within a certain time frame. In this case, the rate limit is set to 5 requests per minute. If a client exceeds this limit, a TooManyRequest response is returned.

2. Asynchronous Processing: The Action.async method is used instead of Action to process the request asynchronously. This means that the server thread is not blocked while waiting for the expensive operation to complete.

3. Timeouts: A timeout is implemented using the recover method on the Future returned by the expensive operation. If the operation takes longer than the specified timeout, a RequestTimeout response is returned.

4. Offloading Expensive Operations: Although not shown in the code, consider offloading the expensive operation to a separate service or background task to avoid impacting the responsiveness of the server. This could be done using a message queue or a separate microservice, for example.

5. Optimizing Expensive Operations: Also not shown in the code, but consider optimizing the expensive operation to reduce resource consumption and processing time. This could involve optimizing database queries, using caching, or reducing the complexity of the operation.

References