nginx alias misconfiguration allowing path traversal

11 minute read

I recently came across an nginx server that had a vulnerable alias configuration which allowed anyone to read files outside the intended directory. In the following post I will describe the misconfiguration and provide demo files so that you can experiment with it yourself.

The general issue was originally highlighted a few years ago in a BlackHat presentation (Breaking Parser Logic!, Orange Tsai) and apparantly first shown even earlier. While the linked presentation only has a couple of slides on this particular issue it’s worth checking out in full.

The docker setup

Let’s say we have a PHP application that should be served through nginx. To quickly get things running we configure our setup via the following docker-compose.yml file:

version: "3.7"

    image: nginx:alpine
      - 8081:80
      - internal
      - ./webapp:/var/www/webapp
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - php

    image: php:fpm-alpine
      - ./webapp:/var/www/webapp
      - internal

    driver: bridge

We have two services, web (nginx) and php, mount our php source code and nginx.conf and have the services on the same internal network.

Our directory structure will look like this:

├── docker-compose.yml
├── nginx.conf
└── webapp
    ├── app
    │   ├── db.php
    │   └── webroot
    │       └── index.php
    └── static
        └── sample.png

The nginx config

Our nginx.conf file will be a fairly simple and standard-looking one which already has the issue baked in (can you see it?):

server {
    server_name _;

    root /var/www/webapp/app/webroot;
    index index.php index.html index.htm;

    access_log /var/log/nginx/php-access.log;
    error_log /var/log/nginx/php-error.log;

    location /assets {
        alias /var/www/webapp/static/;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_pass php:9000;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

The web app

Our demo web app is really just for demonstration purposes and doesn’t do anything useful.

We have webapp/app/db.php (to have a file outside of the webroot that contains some credentials):


$user = 'appuser';
$pass = 'secret';
$db = new PDO('mysql:host=localhost;dbname=test', $user, $pass);

// ...


And then our index.php in the webroot folder to show an intended output of the PHP application:


echo 'Here is the webroot';


Launch the application and see the issue

Now that we have all necessary demo files set up, we can launch our containers:

docker-compose up --build

Going to should now return Here is the webroot, which is the output of index.php in the webroot folder.

As seen in the above nginx config we also have an /assets location pointing to the /static directory. Navigating to gives us the sample image, as expected.

So what’s the issue?

The /assets location directive has no trailing slash and nginx will thus only match /assets and then append whatever is in the request to the final destination path.

If we open we can force a directory traversal and access files in a directory one level down. For our demo app this means we can access the source code of db.php, a sensitive file outside of the webroot. Here, /assets../app/db.php effectively becomes /var/www/webapp/static/../app/db.php.

Note that attempts to force “regular” directory traversals in a requested path, as in /../, are obviously prevented by default.

nginx slash traversal

While we could of course prevent the PHP source code from being returned vs. evaluated, this would not change the fact that we could still access other files one directory down – for example something like /assets../app/.env.

The fix here is to make sure to always set the full path in the location directive, as in location /assets/ (note the trailing slash).

The files

You can access the demo files via this GitHub gist.

Like to comment? Feel free to send me an email or reach out on Twitter.

Did this or another article help you? If you like and can afford it, you can buy me a coffee (3 EUR) ☕️ to support me in writing more posts. In case you would like to contribute more or I helped you directly via email or coding/troubleshooting session, you can opt to give a higher amount through the following links or adjust the quantity: 50 EUR, 100 EUR, 500 EUR. All links redirect to Stripe.