Path Traversal
How ../ in a file name climbs out of the intended folder to read any file the server can, why stripping it fails, and how to resolve-then-check instead.
- Track
- Web security
- Estimated time
- 20 min
- Level
- Intermediate
Before this lesson
Path traversal happens when a server builds a file path out of a name you give it. The sequence ../ means “go up one folder”, so by repeating it you climb out of the folder the feature meant to serve and reach anything the server process can read — configuration, source code, the classic /etc/passwd.
What is the vulnerability?
res.send(fs.readFileSync('/var/www/docs/' + req.query.name, 'utf8'));Ask for welcome.txt and it reads /var/www/docs/welcome.txt. Ask for ../../../etc/passwd and the path becomes:
/var/www/docs/../../../etc/passwd -> /etc/passwdYour name was joined onto the base and the ../ segments walked back out of it. The server reads whatever it lands on.
Why does it happen?
The name is treated as part of a path, but it comes from the user, and paths have a built-in “go up” operator. Joining user text onto a base directory hands the user that operator.
Why stripping fails
Removing ../ seems obvious, but it is a blacklist:
- Strip once:
....//has a../in its middle; remove it and a clean../is left behind. - Strip repeatedly, then decode: if you strip before URL-decoding, an encoded
%2e%2e%2fsurvives the strip and The order the operations run in. Cleaning before decoding lets an encoded payload slip through and become dangerous after the decode..
You exploit both of these — the one-pass strip and the strip-before-decode order — in the medium and hard labs.
How to prevent it
import path from 'node:path';
const base = '/var/www/docs';
const decoded = decodeURIComponent(req.query.name); // decode fully FIRST
const full = path.resolve(base, path.basename(decoded)); // only the file name
if (!full.startsWith(base + path.sep)) return res.status(400).end(); // check AFTER resolving
res.send(fs.readFileSync(full, 'utf8'));Do not clean the input — resolve the final path and then check it is still inside the allowed folder (the check must come after decoding and resolution). Take only the last segment of the name (basename), or better, never expose file paths at all: map an id to a known file on the server side, so there is no path to traverse.
Labs
A file name that climbs out
SolvedEasyBrowser lab
Escape the documents folder and read /etc/passwd.
Enter the labLockedStripped only once
SolvedMediumBrowser lab
Read /etc/passwd again, bypassing the stripping of ../
Enter the labLockedIt strips, then decodes
SolvedHardBrowser lab
Read /etc/passwd despite the repeated stripping, exploiting that decoding comes after it.
Enter the labLocked
