Skip to content
العربية

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.

Estimated time
20 min
Level
Intermediate
Your progress in this track0%

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?

open.js
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:

resolved path
/var/www/docs/../../../etc/passwd   ->   /etc/passwd

Your 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%2f survives 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

safe.js
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.

Review questionA server does name.replace('../', '') once, then reads /var/www/docs/ + name. How is it bypassed?

Labs

  • A file name that climbs out

    EasyBrowser lab

    Escape the documents folder and read /etc/passwd.

  • Stripped only once

    MediumBrowser lab

    Read /etc/passwd again, bypassing the stripping of ../

  • It strips, then decodes

    HardBrowser lab

    Read /etc/passwd despite the repeated stripping, exploiting that decoding comes after it.