7 tips to make your upload API hack proof

Devsecurely Avatar
Please select your technology to adapt the text:

If you allow users to upload files, you need to make sure your website is secure against common file upload attacks. This post helps you identify potential issues and shows you how to fix them.

I will describe the most secure way to implement an upload API. You can apply this directly if you are creating an upload API endpoint from scratch. 

You might already have developed an upload  API, and you find it difficult to alter the existing code to conform to this ideal API controller. We still urge you to follow the directions given in the “file storage” section. You should also make sure to check file access permissions.

Database structure

Yes, you probably need to store information related to the upload API in the database. How else can you keep track of who uploaded what file? We can distinguish various scenarios here:

One user one file

This section is for when your upload API allows the user to upload only one file. For example, this applies to profile picture uploads. A user can only have one profile picture.

This case is very simple, you can just add a column in the user’s database called “profile_picture”.

When a user uploads a new profile picture, you update the user’s “profile_picture” value by storing the file name in it. The file name should be generated randomly as explained in the section below.

One user multiple files

If your users can upload multiple files on your application, you need a dedicated database table to store related data.

This table should have at least 5 columns, you can add more columns if you need to save more context on the files or on the upload operation itself. The 5 essential columns are “file_id”,”user_id”, ”filename”, ”upload_date”, “original_filename”. You can create such a table using the following SQL query:

CREATE TABLE files (

  file_id int NOT NULL AUTO_INCREMENT,

  user_id int NOT NULL,

  original_filename varchar(255) NOT NULL,

  filename varchar(255) NOT NULL,

  upload_date datetime NOT NULL,

  PRIMARY KEY (file_id)

)

 When a user uploads a file, you create a new row in this table. This row should contain the uploading user’s ID, the user’s provided filename, the randomly generated filename (more on that later), and the date of the upload.

Same file multiple users

If your application allows users to upload files, and those files can then be accessed by other users, a more complex access right table might be needed.

Depending on the business logic of the feature, you might not need this table. Use common sense.

For example, if files can be accessed by users in the same company, there is no need to add a table to manage each user’s access rights to the file. Just check, on the download API, if the user belongs to the same company as the user who uploaded the file.

If your application allows users to share their files with specific users (like dropbox), then you might want to create an access control table. This is in addition to the “files” table described in the previous section. The new table should contain, at least, the following rows: “file_permission_id”,”file_id”,”user_id”,”permission”. You can create such a table using the following SQL query:

CREATE TABLE file_permissions (

  file_permission_id INT NOT NULL AUTO_INCREMENT ,

  file_id INT NOT NULL ,

  user_id INT NOT NULL ,

  permission ENUM('READ','DELETE','EDIT','FULL_CONTROL') NOT NULL ,

  PRIMARY KEY (file_permission_id)

)

As described in the previous section, when a user uploads a file, you should create a new row in the files table. Then, add a new permission in the “file_permissions” table. This row should contain the uploading user’s ID, the newly created file_id, and the permission “FULL_CONTROL”.

You should have another API endpoint that allows a user to share his files with other users. When the owner of a file grants another user access to the file, you should add a new row in the permissions table for the designated user and the involved file and the defined role (READ/DELETE/EDIT/FULL_CONTROL).

File storage

Now, we will discuss how we should store the files.

Don’t expose the Upload folder

If we’re not careful, our web server might directly expose the uploaded files. Users can thus guess the name of a file, and access it directly with a URL of the form https://www.example.com/uploads/user_uploaded_file.pdf.

This configuration renders our access control database table completely useless. Independent of a user’s rights over the file, he can download it to his computer. He can download files  even if he doesn’t have an account on your application.

To make our access control solution efficient, we need to make sure our download API is the only way users can access uploaded files. A download API can be as simple as a controller that sends back a user’s profile picture, or as complex as a Dropbox-like download page with file preview.

Depending on your solution, you need to configure the web server (Apache/nginx/Microsoft IIS/Express …) to not directly serve files from the upload folder.

On an apache server, for instance, you can create an .htaccess file inside the “/uploads” folder, containing the following configuration:

Require all denied

Authentication and Authorization

This should be obvious, but here is a reminder:

  • If your application only allows authenticated users to upload files, then check, at the beginning of the upload process, that the user is authenticated.
  • If your application only allows users with a certain privilege to upload files, then check, at the beginning of the upload process, that the user has the required privileges.

Limit file size

To avoid overwhelming your server storage with huge files, set a file size limit for uploaded files. This limit is highly dependent on your application context and what type of files you allow users to upload.

Profile pictures for example do not require a lot of space, you can limit those files to 10MB.

Other files your application can allow might take more space (models and databases for machine learning, video files for streaming websites …). You might want to limit the overall file size allowed per account (like what email providers do), and allow the user to purchase more storage space if needed.

File extension whitelist

If you know what type of files you are expecting, make sure to only allow users to upload files with the expected file extension. If a user tries to upload an image, only accept image file extensions. If a user tries to upload a list of elements, only accept .cvs or .xlsx files …

If possible, try to implement a whitelist strategy, and not a blacklist one. Have a list of accepted extensions ready, and check that the user’s file extension is in that list.

Check the file format

When receiving a file from the user, check if the file conforms to what you were expecting. This is dependent on your application and its context. If you allow users to upload profile pictures, check that the file sent is really a picture. If you were expecting a .csv file to import data, check if the data within the file are in csv format and respect your specs.

Random name generation

Before saving the uploaded file on your server, give it a random file name. The filename should not include any file extension. This removes all the risks associated with trusting the filename that the (potentially malicious) user provides us.

Scan for viruses (optional)

If you allow users to share uploaded files with each other, and you want to protect the users of the platform, you should scan uploaded files for viruses. Malicious users could use your website to store and distribute malicious files. They would hope that other users would download those malicious files and thus infect their computers.

The simplest way to accomplish this, on your application server (or a server you control), is to use ClamAV(https://www.clamav.net/).

Cloud file storage

If you don’t store the uploaded files directly on your server, but store them on a cloud file storage solution (AWS S3, Google Cloud Storage, Azure Blob Storage …), then the above tips still apply.

In Particular, you need to make sure that your storage is not publicly exposed, and that only your application (your application specific API key) can access files on the file storage.

A full implementation

We implemented a full upload API for you using the principles discussed above:

@RequestMapping(value = "/upload",
    		method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)

public ResponseEntity<?> uploadFile(
        @RequestParam("file") MultipartFile file,
		HttpServletRequest request, 
        HttpServletResponse response) {
	
	List<String> doc_whitelist = Arrays.asList("png", "jpg", "jpeg");
	Path upload_folder = Paths.get("/var/www/uploads/");
	
	String original_filename = file.getOriginalFilename();
	String extension = original_filename.substring(original_filename.lastIndexOf(".") + 1);
	
	// Check if user is authenticated
	if ( ! userAuthenticated(request) )
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Login required for this operation");
	
	// Check if the file size exceeds the maximum allowed
	if (file.getSize() > 10485760)
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("File too big, at most 10MB are allowed");
	
	// Check if the file extension is in the allowed extensions
	if( ! doc_whitelist.contains(extension) )
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Forbidden file extension");
	
	// Check the format (adapt to your context): check if file is a valid image
	try (InputStream input = file.getInputStream()) {
	    try {
	        ImageIO.read(input).toString();
	    } catch (Exception e) {
	    	return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Only images accepted");
	    }
	} catch (IOException e1) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	// Generate random file name
	String new_file_name = UUID.randomUUID().toString() + extension;
	
	Path destination_file = upload_folder.resolve(new_file_name);
	
	// Save file on the file system, with the generated name
	try (InputStream inputStream = file.getInputStream()) {
		Files.copy(inputStream, destination_file, StandardCopyOption.REPLACE_EXISTING);
	} catch (IOException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	// Save the file path in the user's database record
	String sql_query = "UPDATE users SET profile_picture = ? WHERE user_id = ?";
	Connection conn;
	try {
		conn = DriverManager.getConnection(getDatabaseUrl(), getDatabaseUsername(), getDatabasePassword());
		PreparedStatement stmt = conn.prepareStatement(sql_query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
    	stmt.setString(1,new_file_name);
    	stmt.setInt(2,getCurrentUserId(request));
    	stmt.executeQuery();
    	
    	conn.close();
	} catch (SQLException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}

	return ResponseEntity.ok("success");
	
}


@RequestMapping(value = "/download", method = RequestMethod.GET)
    public ResponseEntity<?> downloadFile(
    		HttpServletRequest request, 
            HttpServletResponse response) throws FileNotFoundException 
{
	String upload_folder = "/var/www/uploads/";
	String filename = "";
	
	// Check if user is authenticated
	if ( ! userAuthenticated(request) )
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Login required for this operation");
		
	String sql_query = "SELECT profile_picture FROM users WHERE user_id = ?";
	Connection conn;
	try {
		conn = DriverManager.getConnection(getDatabaseUrl(), getDatabaseUsername(), getDatabasePassword());
		PreparedStatement stmt = conn.prepareStatement(sql_query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
    	stmt.setInt(1,getCurrentUserId(request));
    	ResultSet rs = stmt.executeQuery();
    	
    	conn.close();
    	
    	if(rs.next())
        {
        	rs.first();
        	filename = rs.getString("profile_picture");
        }
        else
        {
        	return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
        }
	} catch (SQLException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	
	File file = new File(upload_folder, filename);
	
	response.setContentType("application/force-download");
    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);
}
@RequestMapping(value = "/upload",
    		method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)

public ResponseEntity<?> uploadFile(
        @RequestParam("file") MultipartFile file,
		HttpServletRequest request, 
        HttpServletResponse response) {
	
	List<String> doc_whitelist = Arrays.asList("doc", "docx", "pdf");
	Path upload_folder = Paths.get("/var/www/uploads/");
	
	String original_filename = file.getOriginalFilename();
	String extension = original_filename.substring(original_filename.lastIndexOf(".") + 1);
	
	// Check if user is authenticated
	if ( ! userAuthenticated(request) )
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Login required for this operation");
	
	// Check if the file size exceeds the maximum allowed
	if (file.getSize() > 10485760)
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("File too big, at most 10MB are allowed");
	
	// Check if the file extension is in the allowed extensions
	if( ! doc_whitelist.contains(extension) )
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Forbidden file extension");
	
	// Generate random file name
	String new_file_name = UUID.randomUUID().toString() + extension;
	
	Path destination_file = upload_folder.resolve(new_file_name);
	
	// Save file on the file system, with the generated name
	try (InputStream inputStream = file.getInputStream()) {
		Files.copy(inputStream, destination_file, StandardCopyOption.REPLACE_EXISTING);
	} catch (IOException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	// Save the file in the database table 'files'
	String sql_query = "INSERT INTO files (user_id, filename, upload_date, original_filename) VALUES (?, ?, NOW(), ?)";
	Connection conn;
	try {
		conn = DriverManager.getConnection(getDatabaseUrl(), getDatabaseUsername(), getDatabasePassword());
		PreparedStatement stmt = conn.prepareStatement(sql_query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
		stmt.setInt(1,getCurrentUserId(request));
		stmt.setString(2,new_file_name);
    	stmt.setString(3,original_filename);
    	stmt.executeQuery();
    	
    	conn.close();
	} catch (SQLException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}

	return ResponseEntity.ok("success");
	
}

@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity<?> downloadFile(
		@RequestParam(required = true, value="file_id") int file_id,
		HttpServletRequest request, 
        HttpServletResponse response) throws FileNotFoundException 
{
	String upload_folder = "/var/www/uploads/";
	String filename = "";
	String original_filename = "";
	
	// Check if user is authenticated
	if ( ! userAuthenticated(request) )
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Login required for this operation");
	
	String sql_query = "SELECT filename, original_filename FROM files WHERE user_id = ? AND file_id = ?";
	Connection conn;
	try {
		conn = DriverManager.getConnection(getDatabaseUrl(), getDatabaseUsername(), getDatabasePassword());
		PreparedStatement stmt = conn.prepareStatement(sql_query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
    	stmt.setInt(1,getCurrentUserId(request));
    	stmt.setInt(2,file_id);
    	ResultSet rs = stmt.executeQuery();
    	
    	conn.close();
    	
    	if(rs.next())
        {
        	rs.first();
        	filename = rs.getString("filename");
        	original_filename = rs.getString("original_filename");
        }
        else
        {
        	return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
        }
	} catch (SQLException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	// Escape any carriage return characters to avoid HTTP header splitting
	original_filename = StringUtils.replaceEach (original_filename, 
			new String[] {"
","\n","
","\r","%0d","%0D","%0a","%0A","5"},
			new String[] {"","","","","","","","",""});
	
	File file = new File(upload_folder, filename);
	
	response.setContentType("application/force-download");
    HttpHeaders respHeaders = new HttpHeaders();
    respHeaders.setContentLength(file.length());
    respHeaders.setContentDispositionFormData("attachment", original_filename);

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
}
@RequestMapping(value = "/upload",
    		method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)

public ResponseEntity<?> uploadFile(
        @RequestParam("file") MultipartFile file,
		HttpServletRequest request, 
        HttpServletResponse response) {
	
	List<String> doc_whitelist = Arrays.asList("doc", "docx", "pdf");
	Path upload_folder = Paths.get("/var/www/uploads/");
	
	String original_filename = file.getOriginalFilename();
	String extension = original_filename.substring(original_filename.lastIndexOf(".") + 1);
	
	// Check if user is authenticated
	if ( ! userAuthenticated(request) )
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Login required for this operation");
	
	// Check if the file size exceeds the maximum allowed
	if (file.getSize() > 10485760)
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("File too big, at most 10MB are allowed");
	
	// Check if the file extension is in the allowed extensions
	if( ! doc_whitelist.contains(extension) )
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Forbidden file extension");
	
	// Generate random file name
	String new_file_name = UUID.randomUUID().toString() + extension;
	
	Path destination_file = upload_folder.resolve(new_file_name);
	
	// Save file on the file system, with the generated name
	try (InputStream inputStream = file.getInputStream()) {
		Files.copy(inputStream, destination_file, StandardCopyOption.REPLACE_EXISTING);
	} catch (IOException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	long file_id = 0;
	// Save the file in the database table 'files'
	String sql_query = "INSERT INTO files (user_id, filename, upload_date, original_filename) VALUES (?, ?, NOW(), ?)";
	Connection conn;
	try {
		conn = DriverManager.getConnection(getDatabaseUrl(), getDatabaseUsername(), getDatabasePassword());
		PreparedStatement stmt = conn.prepareStatement(sql_query, Statement.RETURN_GENERATED_KEYS);
		stmt.setInt(1,getCurrentUserId(request));
		stmt.setString(2,new_file_name);
    	stmt.setString(3,original_filename);
    	stmt.executeUpdate();
    	
    	ResultSet rs = stmt.getGeneratedKeys();
    	
    	if (rs.next()) {
    		file_id = rs.getLong(1);
        }
    	
    	conn.close();
	} catch (SQLException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	
	// Add FULL_CONTROL permission to the user on the file
	sql_query = "INSERT INTO file_permissions (file_id, user_id, permission) VALUES (?, ?, 'FULL_CONTROL')";
	try {
		conn = DriverManager.getConnection(getDatabaseUrl(), getDatabaseUsername(), getDatabasePassword());
		PreparedStatement stmt = conn.prepareStatement(sql_query, Statement.RETURN_GENERATED_KEYS);
		stmt.setLong(1,file_id);
		stmt.setInt(2,getCurrentUserId(request));
		
    	stmt.executeUpdate();
    	
    	conn.close();
	} catch (SQLException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}

	return ResponseEntity.ok("success");
	
}
    
@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity<?> downloadFile(
		@RequestParam(required = true, value="file_id") int file_id,
		HttpServletRequest request, 
        HttpServletResponse response) throws FileNotFoundException 
{
	String upload_folder = "/var/www/uploads/";
	String filename = "";
	String original_filename = "";
	String permission = "";
	
	// Check if user is authenticated
	if ( ! userAuthenticated(request) )
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Login required for this operation");
	
	
	// Check the user's permission on the file
	String sql_query = "SELECT permission FROM file_permissions WHERE user_id = ? AND file_id = ?";
	Connection conn;
	try {
		conn = DriverManager.getConnection(getDatabaseUrl(), getDatabaseUsername(), getDatabasePassword());
		PreparedStatement stmt = conn.prepareStatement(sql_query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
    	stmt.setInt(1,getCurrentUserId(request));
    	stmt.setInt(2,file_id);
    	ResultSet rs = stmt.executeQuery();
    	
    	
    	if(rs.next())
        {
        	rs.first();
        	permission = rs.getString("permission");
        }
        else
        {
        	return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access denied");
        }
	} catch (SQLException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	if(permission != "FULL_CONTROL" && permission != "READ")
		return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access denied");
	
	sql_query = "SELECT filename, original_filename FROM files WHERE user_id = ? AND file_id = ?";
	try {
		PreparedStatement stmt = conn.prepareStatement(sql_query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
    	stmt.setInt(1,getCurrentUserId(request));
    	stmt.setInt(2,file_id);
    	ResultSet rs = stmt.executeQuery();
    	
    	conn.close();
    	
    	if(rs.next())
        {
        	rs.first();
        	filename = rs.getString("filename");
        	original_filename = rs.getString("original_filename");
        }
        else
        {
        	return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
        }
	} catch (SQLException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	// Escape any carriage return characters to avoid HTTP header splitting
	original_filename = StringUtils.replaceEach (original_filename, 
			new String[] {"
","\n","
","\r","%0d","%0D","%0a","%0A","5"},
			new String[] {"","","","","","","","",""});
	
	File file = new File(upload_folder, filename);
	
	response.setContentType("application/force-download");
    HttpHeaders respHeaders = new HttpHeaders();
    respHeaders.setContentLength(file.length());
    respHeaders.setContentDispositionFormData("attachment", original_filename);

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

Path traversal

You might have an existing upload API where you  absolutely need to save uploaded files under the name that the user provides. In that case, you need to protect your application from path traversal attacks. As we always say, user input is evil. You can’t trust the user to not alter the file name. We’ll use an example to explain this attack.

Let’s take the following vulnerable file upload controller:

@RequestMapping(value = "/upload",
    		method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)

public ResponseEntity<?> uploadFile(
        @RequestParam("file") MultipartFile file,
		HttpServletRequest request, 
        HttpServletResponse response) {
	
	String upload_folder = "/var/www/uploads/";
	
	Path destination_file = Paths.get(upload_folder + file.getOriginalFilename());
	
	// Save file on the file system
	try (InputStream inputStream = file.getInputStream()) {
		Files.copy(inputStream, destination_file, StandardCopyOption.REPLACE_EXISTING);
	} catch (IOException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	return ResponseEntity.ok("success");
	
}

It might seem that the API endpoint puts uploaded files inside the /var/www/uploads/ folder. However, when using the ../ characters, we can circumvent this restriction and escape the path we were assigned to. In Linux, the characters ../ allow us to go up the folder hierarchy. The same can be achieved in a Windows server using the ..\ characters.

For reference, here’s what a raw HTTP file upload request would look like:

Example HTTP request for file upload

We can see that the user can put any value he wants as the filename. Thus, he could upload a file with the name “../index.html”. Our API controller will save the file, on the server, using the path “/var/www/uploads/../index.html”. This path resolves to “/var/www/index.html”, and the controller thus replaces the index.html file with the file provided by the user.

By exploiting this vulnerability, the attacker can create new files, or even replace existing files.

What’s the risk of path traversal?

The worst case scenario, the user can compromise your whole application. He might upload files that get interpreted by your webserver.

For instance, your web server might be configured to interpret PHP files. Suppose the user uploads a malicious PHP file on a folder exposed by the web server, for example "/var/www/uploads/../script.php". The attacker can execute the malicious PHP script by visiting the script URL https://www.example.com/script.php in his browser.

Malicious server-side scripts can retrieve server credentials, directly access the database, steal user credentials …

If server-side code interpretation is somehow not possible, then the user can still upload malicious HTML pages. These pages contain malicious Javascript code that runs on the victims’ browsers. When a user of your application visits one of these pages, the malicious Javascript can force his browser to execute actions on the application, using the victim’s identity on the website (delete objects, give permissions to the attacker, change the victim’s password …).

How to fix path traversal?

If possible, don’t save the file using the filename that the user provides. Generate a random filename with no extension, and use that to save the file. You can store the original filename in the database to use when the user downloads his file.

If this solution is not possible for you, then you need to check the real path of the filename.

The real path is the filesystem path that you get after resolving all the path traversal characters. For example, the real path of "/var/www/upload/../../../../../../../etc/passwd" is "/etc/passwd".

The real path is a string that needs to start with the upload folder you defined. If not, you need to abort the upload operation and return an error message. Here’s how you could implement this fix on the controller shown earlier:

@RequestMapping(value = "/upload",
    		method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)

public ResponseEntity<?> uploadFile(
        @RequestParam("file") MultipartFile file,
		HttpServletRequest request, 
        HttpServletResponse response) {
	
	String upload_folder = "/var/www/uploads/";
		
	String canonicalPath = new URI(upload_folder + file.getOriginalFilename()).normalize().getPath();
	
    if (! canonicalPath.startsWith(upload_dir))
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Path traversal attempt");
	
	Path destination_file = Paths.get(canonicalPath);
	
	// Save file on the file system
	try (InputStream inputStream = file.getInputStream()) {
		Files.copy(inputStream, destination_file, StandardCopyOption.REPLACE_EXISTING);
	} catch (IOException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
	}
	
	return ResponseEntity.ok("success");
	
}

Tagged in :

Devsecurely Avatar

Leave a Reply

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