nginx alias misconfiguration allowing path traversal
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"
services:
web:
image: nginx:alpine
ports:
- 8081:80
networks:
- internal
volumes:
- ./webapp:/var/www/webapp
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
php:
image: php:fpm-alpine
volumes:
- ./webapp:/var/www/webapp
networks:
- internal
networks:
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):
<?php
$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:
<?php
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 http://127.0.0.1:8081
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 http://127.0.0.1:8081/assets/sample.png
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 http://127.0.0.1:8081/assets../app/db.php
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.
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.