Web Vulnerability 101: Arbitrary File Upload / Directory Traversal (Java with Spring)

In this section, we will look at a server side vulnerability that is often a highly sought after feature in a web application as it provides a platform for attackers to try and upload a malicious file for execution.

The vulnerable application is written in Java with Spring Web MVC framework (current version used at the time of posting is 5.0.2.RELEASE). It was written insecurely to demonstrate a common set of file upload security bugs and to further look at how they can be fixed.

Vulnerable File Upload Controller

@RequestMapping(value = "/upload", method = RequestMethod.POST)
  public String uploadFileHandler(ModelMap model, @RequestParam("file") MultipartFile file, @RequestParam("name") String name) {
    if (!file.isEmpty()) {
      try {
	byte[] bytes = file.getBytes();

	// Creating the directory to store the uploaded files
	String rootPath = System.getProperty("catalina.home");
	File dir = new File(rootPath + File.separator + "tmpFiles");
	if (!dir.exists())
	  dir.mkdirs();

	// Create the file on server
	File serverFile = new File(dir.getAbsolutePath() + File.separator + name + "." + FilenameUtils.getExtension(file.getOriginalFilename()));
				
	BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(serverFile));
	stream.write(bytes);
	stream.close();

	String path = "File has been uploaded to <b>" + serverFile;
	model.addAttribute("path", path);
	return "upload";
      } catch (Exception e) {
	return "You failed to upload " + "" + " => " + e.getMessage();
      }
    } else {
	return "You failed to upload " + name+ " because the file was empty.";
    }
  }

Try to identify the bugs and see if you get any correct before continuing down!


What's wrong with the controller?

  1. Arbitrary file upload
    The only file validation that the controller does is to check if the file is empty. It does not perform any file type validation to ensure an application-specific file that is allowed to be uploaded.

    if (!file.isEmpty()) {
      try {
        byte[] bytes = file.getBytes();
     	// Creating the directory to store the uploaded files
    	String rootPath = System.getProperty("catalina.home");
        File dir = new File(rootPath + File.separator + "tmpFiles");
        ...
        ..
        .
    

    This would allow an attacker to upload any file type from web shells to executables that, if executed, could lead to a reverse shell connection going out to the attacker's machine.

  2. Directory traversal
    The controller calls the new method of File to create a new file but taking the user-supplied and untrusted user input parameter of name and directly concatenating it to form the filename to be saved.

    // Create the file on server
    File serverFile = new File(dir.getAbsolutePath() + File.separator + name + "." + FilenameUtils.getExtension(file.getOriginalFilename()));
    

A common attack is to perform the dot-dot-slash attack where an attacker tries to traverse out of the current directory to download or upload file.
fileupload_1

If an attacker supplies a "../../evil.jsp" as the filename, the file would be created two directories up, at ~/spring/target/evil.jsp instead of in ~/spring/target/tomcat/tmpFiles/evil.jsp.
fileupload_2

Such attack usually aims at overwriting critical files in the server. In the same manner, a vulnerable download function would allow the attackers to download sensitive files from the server.

  1. Disclosure of upload path
    The application prints out the internal working path of the location of uploaded files to the browser.
    fileupload_3
    This helps the attacker to navigate around the directory as it also reveals the application / web server directory format.

The issues above are just some of the more obvious potential issues that can lead to security risks.


The vulnerabilities are fixed in the code snippet below:

@RequestMapping(value = "/upload-soln", method = RequestMethod.POST)
  public String uploadFileHandlerWhitelist(ModelMap model, @RequestParam("file") MultipartFile file) {
    String ext = FilenameUtils.getExtension(file.getOriginalFilename());
    String mime = file.getContentType();

    if (!file.isEmpty()) {
      // Check Content-Type / Mime-Type and Extension
      if(!mime.equalsIgnoreCase("application/vnd.openxmlformats-officedocument.wordprocessingml.document") || !"docx".equals(ext)){
    String error = "File not supported!";
    model.addAttribute("path", error);
    return "upload-soln";
      }

      try {
    byte[] bytes = file.getBytes();
    String name = file.getOriginalFilename();

    // Creating the directory to store uploaded file
    String rootPath = System.getProperty("catalina.home");
    File dir = new File(rootPath + File.separator + "tmpFiles");
    if (!dir.exists())
      dir.mkdirs();
          // Create the file on server
      File serverFile = new File(dir.getAbsolutePath() + File.separator + name);

      BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(serverFile));
      stream.write(bytes);
      stream.close();

          String path = "File has been uploaded to <b>" + serverFile;
      model.addAttribute("path", path);
      return "upload-soln";
        } catch (Exception e) {
      String errMsg = "You failed to upload " + "" + " => " + e.getMessage();
      model.addAttribute("path", errMsg);
      return "upload-soln";
    }
      } else {
    String emptyFile = "Empty file!";
        model.addAttribute("path", emptyFile);
    return "upload-soln";
      }
  }

What other checks do you think we need to implement?

  1. Arbitrary file upload (fix)
    After validating that the file is not empty, the Controller further checks the Content-Type / Mime-Type and Extension. In this case, if it is not of the document type specifically specified in the check, it will redirect the user to the previous page.

    if(!mime.equalsIgnoreCase("application/vnd.openxmlformats-officedocument.wordprocessingml.document") || !"docx".equals(ext)){
    String error = "File not supported!";
    model.addAttribute("path", error);
    return "upload-soln";
    }...
    
  2. Directory traversal (fix)
    Spring's getOriginalFileName() method of the CommonsMultiPartFile Class strips file paths (Ref: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/MultipartFile.html)

    String name = file.getOriginalFilename();
    ...

    No longer appends untrusted user supplied name into the path and relies fully on the filename as the name of the file.
    File serverFile = new File(dir.getAbsolutePath() + File.separator + name);

    If name is to be used, validate parameter using .replaceAll method to replace any . with empty space like so:
    fileupload_4.JPG

  3. Disclosure of upload path (fix)
    Do not return any unnecessary information! We simply remove the printing of the paths to the user. Simply notifies the user of the successful upload is more than enough.

Please also note the following:

  • If you are accepting files a storage service, remember to set HTTP Response Header Content-Disposition to ensure that the browser does not render the file on the browser but rather prompts the user for download:
    fileupload_5.JPG
  • Accepting images? Implement Java imageio to perform type check. It supports BMP, GIF, JPEG, PNG and WEBP with no external dependencies! (Ref: https://docs.oracle.com/javase/7/docs/api/javax/imageio/package-summary.html)
  • Always check for the limit of files. This can be done on your Spring's DispatcherServlet XML by setting the maxium upload size accepted.

fileupload_6
In this case, the application would only accept files with the maximum size of 100KB!
In the sample application we have been looking at, the following error would be returned if you upload anything more than 100KB!
fileupload_7

Summary!

File upload vulnerability varies between applications depending on how the uploaded file are treated. The simple rule to follow is still to always validate user input. If you render Excel files into tables, make sure that you validate each parameter before rendering them!

Web Vulnerability 101: Arbitrary File Upload / Directory Traversal (Java with Spring)
Share this