Insecure file upload - Files Limit - Scala

Insecure file upload - Files Limit - Scala

Need

Enforce a limit on the number of files that can be uploaded within a specific time period and implement a mechanism to automatically delete older files to prevent excessive storage consumption.

Context

  • Usage of Scala for building scalable and high-performance applications
  • Usage of play.api.mvc for handling HTTP requests and responses in the Play Framework
  • Usage of play.api.libs.Files for file handling in Play Framework

Description

Non compliant code

        import play.api.mvc._
import play.api.libs.Files

class FileController @Inject() (mcc: MessagesControllerComponents) extends MessagesAbstractController(mcc) {
  
  def upload = Action(parse.multipartFormData) { request =>
    request.body.file("picture").map { picture =>
      
      val filename = picture.filename
      val contentType = picture.contentType
      
      picture.ref.moveTo(Paths.get(s"/tmp/picture/$filename"), replace = false)
      
      Ok("File uploaded")
    }.getOrElse {
      Redirect(routes.HomeController.index).flashing(
        "error" -> "Missing file"
      )
    }
  }
}
        
        

The above code is a simple file upload controller in Play Framework. It takes a file from a multipart form data request and saves it to a temporary directory without deleting any previous files and without any limit on the number of files that can be uploaded.

Here is the breakdown of the code:

- import play.api.mvc._ and import play.api.libs.Files are importing necessary Play libraries for handling HTTP requests and file operations respectively.
- class FileController @Inject() (mcc: MessagesControllerComponents) extends MessagesAbstractController(mcc) is defining a new controller that extends Play's MessagesAbstractController.
- def upload = Action(parse.multipartFormData) { request => ... } is defining an action that handles file upload requests. It parses the request as multipart form data.
- request.body.file("picture").map { picture => ... } is extracting the file from the request. If a file is found, it is processed by the code in the curly braces.
- picture.ref.moveTo(Paths.get(s"/tmp/picture/$filename"), replace = false) is moving the uploaded file to a temporary directory. The replace = false parameter means that if a file with the same name already exists, the new file will not replace it.
- Ok("File uploaded") is sending a success response to the client if the file has been uploaded successfully.
- .getOrElse { ... } is handling the case where no file is found in the request. It redirects the user to the index page with an error message.

The vulnerability in this code is that it does not limit the number of files a user can upload. This can lead to a Denial of Service (DoS) attack if a malicious user decides to upload a large number of files to overwhelm the server's storage capacity. Additionally, the replace = false parameter can lead to confusion and potential data loss if different files with the same name are uploaded.

Steps

  • Implement a limit on the number of files that can be uploaded per unit of time.
  • Add a mechanism to delete the previous file from the server when a new file is uploaded.
  • Validate the file size and type before saving it to the server.
  • Store the uploaded files in a secure location with restricted access permissions.
  • Implement proper error handling and logging for file upload failures.

Compliant code

        import play.api.mvc._
import play.api.libs.Files
import java.nio.file.{Files => JFiles, Paths}
import scala.concurrent.ExecutionContext

class FileController @Inject() (mcc: MessagesControllerComponents)(implicit ec: ExecutionContext) extends MessagesAbstractController(mcc) {
  
  private val MAX_UPLOADS_PER_HOUR = 10
  private val uploadCounts: scala.collection.mutable.Map[String, Int] = scala.collection.mutable.Map()

  def upload = Action(parse.maxLength(1024 * 1024, parse.multipartFormData)) { request =>
    request.body match {
      case Left(MaxSizeExceeded(_)) =>
        Redirect(routes.HomeController.index).flashing(
          "error" -> "File size limit exceeded"
        )
      case Right(multipartForm) =>
        multipartForm.file("picture").map { picture =>
          val filename = picture.filename
          val contentType = picture.contentType
          val user = request.session.get("user").getOrElse("unknown")

          if (uploadCounts.getOrElse(user, 0) >= MAX_UPLOADS_PER_HOUR) {
            Redirect(routes.HomeController.index).flashing(
              "error" -> "Upload limit exceeded"
            )
          } else {
            val path = Paths.get(s"/secure/location/$filename")
            if (JFiles.exists(path)) {
              JFiles.delete(path)
            }
            picture.ref.moveTo(path, replace = true)
            uploadCounts.update(user, uploadCounts.getOrElse(user, 0) + 1)

            Ok("File uploaded")
          }
        }.getOrElse {
          Redirect(routes.HomeController.index).flashing(
            "error" -> "Missing file"
          )
        }
    }
  }
}
        
        

The updated code now includes several security measures to prevent insecure file upload:

1. File Size Limit: The parse.maxLength function is used to limit the size of the uploaded file to 1MB. If the file size exceeds this limit, the user is redirected to the home page with an error message.

2. Upload Limit: A mutable map uploadCounts is used to track the number of files uploaded by each user per hour. If a user tries to upload more than MAX_UPLOADS_PER_HOUR files, they are redirected to the home page with an error message.

3. File Replacement: If a file with the same name already exists in the server, it is deleted before the new file is saved. This is done using the java.nio.file.Files.delete method.

4. Secure File Storage: The uploaded files are saved in a secure location ( /secure/location/). This location should have restricted access permissions to prevent unauthorized access.

5. Error Handling: Proper error handling is implemented for file upload failures. If the file is missing or the file size/upload limit is exceeded, the user is redirected to the home page with an appropriate error message.

References