Path Traversal and File Disclosure
Getting the Server to Read What You Want
Path traversal (directory traversal) is a file-read flaw: attacker-controlled file paths escape the intended directory and reach files the application should never serve — /etc/passwd, application config with secrets, database files, or the private key of the very server you are testing. It is quiet, easy to automate, and often the on-ramp to source disclosure that powers the next chain.
The Escape Syntax That Works Everywhere
The attack surface extends far past the explicit "read file" feature, because anything that passes a user string to a filesystem lookup is a traversal candidate — and that includes archive extraction and symlink reads. The bug lives in naive path building:
# VULNERABLE
content = open(f"/var/www/uploads/{user_filename}").read()
If user_filename is ../../etc/passwd, the filesystem happily resolves the combined path to /etc/passwd — the .. climbs out of uploads towards the root. Encoded/obfuscated variations (URL-decoded %2e%2e%2f, double-encoding, Unicode overlongs, ....// after a collapsing pass, and on Windows backslashes and alternate streams) slip past naive sanitizers that only strip a literal ../. An attacker iterating payloads with a fuzzer finds the working one in seconds, which is why traversal must be fixed structurally, not by a blacklist of ../. Blacklists are doubly cursed: they miss encoded forms and false-positive on legitimate names that happen to contain a dot-dot sequence, forcing operators to weaken the rule in production. Accept only what the canonicalized check permits, and let the deny be absolute.
A Whole Class of File Logic
Traversal is one member of a larger family of file-descriptor bugs: absolute path injection (the attacker supplies /etc/passwd directly and the app appends a prefix that is irrelevant), symlink attacks (an attacker plants a symlink in a writable dir pointing the read at an unreachable file), and zip-slip-style archive writes (a crafted archive extracts ../../ filenames to overwrite files outside the extraction directory). Review the moment any user-supplied path meets filesystem access — the "download an attachment" endpoint, the "process this uploaded file" worker, the archive extractor — because each one is a traversal candidate.
The Fix: Canonicalize and Confine
The robust pattern: resolve the full path, verify it is still inside the intended root, and refuse otherwise:
from pathlib import Path
root = Path("/var/www/uploads")
candidate = (root / user_filename).resolve()
if not candidate.is_relative_to(root):
raise PermissionError("path escapes the allowed directory")
content = candidate.read_bytes()
resolve() collapses every .. and symlink to the real path; is_relative_to(root) then decides if the result could be legitimate. Even with the check, serve identifiers, not paths: the bulletproof design is a manifest — an allowlist mapping user IDs (file_42) to stored, server-generated names — so the attacker literally cannot reference a path, only a key you know. Keep upload roots on a separate volume (e.g., object storage) reachable only by the app account, and set Content-Disposition with a sanitized filename on download. The firewall lesson's segmentation makes /etc and the app's working directory different trust zones, which is the belt to the manifest's suspenders.