The Developer’s Checklist: Ensuring Your Download APIs are Hack-Proof

Attacker stealing documents
Devsecurely Avatar
Please select your technology to adapt the text:

If your application allows users to download files (documents, data, pictures…), be cautious of potential issues. Particularly, you need to make sure users can’t download files they’re not supposed to.

Path Traversal

Path Traversal Theory

If your application allows the user to specify the name of the file to be downloaded, you are concerned by this vulnerability. As we always say, user input is evil. You can’t trust the user to not alter the file name.

To simplify the explanation, we will suppose that your download API endpoint takes a parameter called “filename” as an input. A simple way to develop this endpoint is to read the file having the name “filename”, and return the content to the users. This would look something like the following:

@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity getFile(
        @RequestParam(required = false, defaultValue = "/dev/null", value="filename") String filename, 
        HttpServletRequest request, 
        HttpServletResponse response) throws FileNotFoundException 
{
    response.setContentType("application/force-download");
    String upload_dir="/var/www/uploads/";
    File file = new File(upload_dir,filename);
    
    HttpHeaders respHeaders = new HttpHeaders();
    respHeaders.setContentLength(file.length());
    respHeaders.setContentDispositionFormData("attachment", filename);

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
}

But, the user being evil and all, will alter the parameter filename. It might seem that the API endpoint only allows files inside the /var/www/uploads/ folder to be downloaded. However, When we use the ../ characters, we can bypass this restriction. It allows us to escape the assigned path. In Linux, the characters ../ allow us to go up the folder hierarchy. We can achieve the same in a Windows server using the ..\ characters.

Of course, the attacker doesn’t know the upload path. So he would not know how many folders separate the uploads folder from the root directory in Linux. But, by adding enough ../ characters, eventually, he will get there. Once there, you can specify any absolute path you want, and the API will download that file for you.

So the previous path /var/www/uploads/../../../../../../../../../etc/passwd is equivalent to /etc/passwd. So, if the parameter “filename” contains the string “../../../../../../../../../etc/passwd”, the attacker would retrieve the server’s /etc/passwd file.

The Consequences of Path Traversal

Getting the /etc/passwd file is bad. An attacker can also retrieve sensitive files, like application configuration files. Those can contain API keys to other services (payment processor, CRM, Google services …) and sensitive credentials (database, FTP, SMTP or other credentials).

The attacker could also retrieve files that belong to other users. Files that could contain personal or sensitive data.

How to Fix Path Traversal

You should never trust input sent by the user. You need to check that the input doesn’t deviate from expected behavior.

For the best security, don’t allow the user to provide the name of the file to download. He should provide a file ID for example, and you do the conversion from file ID to file name on the server side. To learn more consult our secure upload guide.

If your application relies on filenames, not all hope is lost. You can still make sure users can only download files in a certain folder. To do that, you need to retrieve the real path of the requested file using the function getCanonicalPath(). The real path contains the absolute file system path of the file. You then need to check that that real path, as a string, starts with the upload folder we defined for our users. Below is an example code on how to achieve that:

@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity getFile(
        @RequestParam(required = false, defaultValue = "/dev/null", value="filename") String filename, 
        HttpServletRequest request, 
        HttpServletResponse response) throws FileNotFoundException 
{
    response.setContentType("application/force-download");
    String upload_dir="/var/www/uploads/";
    File file = new File(upload_dir,filename);
    
    String realPath = file.getCanonicalPath();
    if (! realPath.startsWith(upload_dir))
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access denied");
    
    HttpHeaders respHeaders = new HttpHeaders();
    respHeaders.setContentLength(file.length());
    respHeaders.setContentDispositionFormData("attachment", filename);

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
}

File access permissions

You need to make sure that malicious users can’t download user files they shouldn’t have access to. 

Multi-user access

This section is for platforms that allow multiple users to access the same file (users can share files between them, users in the same team can access all the files, …).

You should have a way to get the list of users who can access a certain file. The cleanest way to achieve this is to have an access control table in the database. The groundwork for this has to be done on the upload part, by following our secure upload guide.

For each file download request, the controller needs to check whether the user has read permission on the file. This could be achieved with the following example SQL query

SELECT * FROM file_permissions WHERE user_id=:session_user_id AND file_id=:requested_file_id AND read_permission=1;

If the query returns no records, abort the download operation and display an error message.

Single user access

If your platform does not allow multi-user access to the same file, then the process is simple. You need only to check if the user requesting access to the file, is the same user that uploaded the file. For this, you need to keep track, in the database, which user uploaded each file.

Download, don’t Interpret

What would happen if any user could create new pages on your website? It would be catastrophic, wouldn’t it? An attacker could create new pages with fraudulent forms to steal other users’ credentials. He could also create HTML files containing malicious Javascript code. That code would be executed by other users who try to download that file.

When users download files, you need to tell their browser not to interpret them. Else, these files could force the victims’ browsers to execute malicious Javascript code. You can do this by including the header Content-Disposition in the API response. It should have the following value: Content-Disposition: attachment; filename="fileName.ext"

Devsecurely Avatar

Leave a Reply

Your email address will not be published. Required fields are marked *