Laravel Path Traversal
Guide: Examples
and Prevention

stackhawk

StackHawk|July 16, 2021

Learn about Laravel path traversal attacks, where they can happen on Laravel-based websites, and how to prevent them.

So you've deployed your Laravel-based website, and you want people to find and see it. However, keep in mind that there are files your website needs to run that you don't want users to see, ever. Those could be PHP files containing configuration data, such as database credentials and API keys. They could be private keys for your web server's SSL certificate. Or, if you have an interactive website with user-generated content, there may be files that some users have uploaded for personal or limited use. No other user should have access to these files or be able to read, overwrite, or delete them without permission. 

This article will examine path traversal attacks, a common problem through which websites leak internal files. We'll discuss where they can happen on Laravel-based websites and how to prevent them.

Add Automated Security Testing to Your Pipeline

What Is a Path Traversal Attack?

Like XSS (cross-site-scripting) and SQL injection attacks, the attack vector of a path traversal attack is user-provided information that the server uses without proper and prior sanitization. With XSS, attackers rely on the website executing their input as JavaScript. And with SQL injections, attackers attempt to modify raw database queries. With a path traversal attack, the attackers want the webserver to treat their input as a file name with path information. This way, they can instruct the web application to reveal or modify any file as long as PHP can access it, even when an end user isn't supposed to. 

There are two ways attackers can provide unwanted path information. One uses an absolute path starting from the root of the operating system ("/"). The other way uses relative paths with the two-dot notation for going up one directory in the hierarchy ("../"). Because of the latter, a path traversal attack is also called the "dot-dot-slash" attack. Other names that OWASP lists for this attack include "directory climbing," "directory traversal," and "backtracking."

Laravel Path Traversal Guide: Examples and Prevention image

Understanding Laravel Directory Structure

As a prerequisite to understanding the scope and potential weak points for path traversal attacks in Laravel, let's look at the framework's directory structure. When you create a new Laravel project, you'll find a couple of files and directories. 

In your webserver configuration, there is a root directory for your Laravel project. It isn't the directory for the project itself but rather the public subdirectory. As a result, the webserver can directly deliver any file in the public subdirectory without involving Laravel. Files in other directories like vendor, config, and storage are invisible to the web server, so they're protected from direct access. They're still potential victims of path traversal attacks, as we'll learn in a bit. In the public subdirectory, you'll also find the index.php file that acts as the entry point for the web application. Your HTML pages and API routes all go through this file. 

Assuming your website is example.com, requesting https://example.com/file.jpg would deliver file.jpg from the public directory if it exists. A request for https://example.com/path/to/page would go to index.php since that path doesn't exist as a file in the public directory, and Laravel would handle it according to the routes you configured. 

Could you break out of the public subdirectory, for example, by requesting https://example.com/../config/app.php? Luckily the answer is no. Webservers like Apache or Nginx already protect you from breaking out of the root directory (the public one). And all the other paths are handled by index.php and Laravel routes, which don't directly correspond to files. So where is the problem?

Laravel File Delivery

Sometimes you want to deliver a file that you've stored outside the public directory to a user. A typical reason is that you want to make files available only to authenticated users. Laravel supports this type of behavior with the download() function on its Response object. Here's an example route implementation for downloading a specific file from the storage/app directory:

Route::get('/download/PrivateDocument', function() {
  return response()->download(storage_path('app/PrivateDocument.pdf'));
});

The previous code example is perfectly safe because there is no user-provided parameter that could cause problems. It also demonstrates the storage_path() function to access the storage directory without building the path yourself. Now, imagine you have multiple files in your storage/app directory. In this case, you may create a parameterized download function such as the following:

Route::get('/download', function(\Illuminate\Http\Request $request) {
  return response()->download(storage_path('app/'.$request->get('filename')));
});

Testing your code for functionality would show that this is working (if you only try for valid cases). To get the same file as in the first example, a user would enter the following URL: https://example.com/download?filename=PrivateDocument.pdf

The Problem

Next, put on your black hat and try to do something unauthorized. How about the following URL: https://example.com/download?filename=../../.env 

Congratulations, you just downloaded the file with the environment variables, containing possibly all your precious credentials!

Since there was no check for a path traversal attack, you were able to move outside the storage/app directory and get an internal project file. Now, how can you prevent this?

The Solution

If you know that all relevant files are in the same directory, you can use PHP's built-in basename() function to strip off any path information from the file name:

Route::get('/download', function(\Illuminate\Http\Request $request) {
  $filename = basename($request->get('filename'));
  return response()->download(storage_path('app/'.$filename));
});

The solution, as mentioned above, requires only a single function and works very well. It has the downside that it strips all path information, including some you may want to accept. For example, you may have files in subdirectories. The following URL would fail, even if the file storage/app/user1/document.pdf existed, because it would look for storage/app/document.pdf: https://example.com/download?filename=user1/document.pdf

Laravel Path Traversal Guide: Examples and Prevention image

We can solve that with another PHP function called realpath(). Unlike basename(), which modifies a string and removes its prefix, realpath() also searches for the file in question. It returns a Boolean false if the file doesn't exist, which you can use to produce a proper error message. However, if the file exists, it returns its entire path, resolving any dot-dot-slash notation in the process. For example, if you're in the storage/app directory and enter ../filename, it returns storage/filename

After using realpath(), you can check if the allowed path is a prefix of the entered filename and take necessary action if it isn't. The following code example illustrates that. It first assigns the approved directory to $basepath, then reads the filename and processes it with realpath(), and finally checks whether the file exists and also that it lives in the $basepath:

Route::get('/download', function(\Illuminate\Http\Request $request) {
  $basepath = storage_path('app');
  $filename = realpath($basepath.'/'.$request->get('filename'));
  if ($filename !== false && substr($filename, 0, strlen($basepath)) == $basepath)
    return response()->download($filename);

  App::abort(404);
});

The example returns a "404 Not Found" error for both nonexistent files and invalid file names.

Other Scenarios

I've shown an example with Laravel's download() function and used Laravel's storage_path() helper in the previous sections. However, that doesn't mean the problem occurs only in this scenario.

Code that reads and writes files from any directory might suffer from path traversal attacks if any part of the file name is user-generated.

To find occurrences in your code, you should search your project for any files that include the Illuminate\Support\Facades\Storage class, which is Laravel's standard way of interacting with the file system. Investigate usage of the Storage façade and where it gets the file and directory names. In addition, search for standard PHP functions that interact with files, such as file_get_contents() and file_put_contents(). Once you find them, apply one of the solutions I explained before. 

By the way, since basename() and realpath() are core PHP functions, not Laravel functions, you can use them in other PHP applications as well.

Alternative Approaches

My previous examples solved the issue by sanitizing file name inputs. While the suggestions fix your path traversal problems, you may prevent them from occurring in the first place through different approaches. I want to mention two of them briefly. 

The first approach takes us back to the first code example. If you only have one or a handful of files, hardcode their file names in your code and create a separate download route for each of them. 

The second approach helps with user-generated content. When you let users upload files, give them randomly generated file names and store a mapping in your database. When it's time for retrieval, search for the file name in the database first, then use the information stored in the column, not the user input, to retrieve the file. The database table can optionally handle access control lists. A complete explanation of such a solution is beyond the scope of this post, though. 

Find and Fix Security Vulnerabilities

To Summarize Laravel Path Traversal Attacks

Path traversal attacks allow users to access the internal files of your application or on the web server. Accessing the file system with unsanitized user input causes these problems. When avoiding file names as user input is not an option, you can use two helpful core PHP functions to check your input and make sure they don't contain paths outside the directories you want. 

Apart from path traversal, XSS, and SQL injections, another type of vulnerability you should always keep in mind is CSRF (cross-site request forgery), which we covered in detail here

This post was written by Lukas Rosenstock. Lukas is an independent PHP and web developer turned API consultant and technical writer. He works with clients of all sizes to design, implement, and document great APIs. His focus is on creating integrations and mashups that highlight the values of APIs, and educate developers about their potential.


StackHawk  |  July 16, 2021