Description
Path traversal (also called directory traversal) is a vulnerability classified under CWE-22 — Improper Limitation of a Pathname to a Restricted Directory. It occurs when an application uses user-controlled input to construct a file system path without adequately canonicalizing or validating that the resulting path stays within the intended base directory. An attacker who can influence the path can use ../ sequences (or their encoded equivalents) to escape the intended directory and access arbitrary files on the server's file system.
Path traversal is a member of the A01:2021 — Broken Access Control category in the OWASP Top 10. While it may appear simple, it is consistently rediscovered in production systems — particularly in file download endpoints, image loaders, log viewers, template renderers, and any feature that maps a URL parameter directly to a file on disk. Network devices such as VPN concentrators, routers, and embedded systems have historically been heavily affected, with several high-profile CVEs in recent years stemming from this exact class.
How It Works
Consider a file download endpoint that serves user-uploaded documents:
GET /download?file=report.pdf HTTP/1.1
Host: app.example.com
Internally, the application constructs:
file_path = os.path.join("/var/www/uploads", request.args.get("file"))
with open(file_path, "rb") as f:
return f.read()
An attacker replaces report.pdf with a traversal sequence:
GET /download?file=../../../../etc/passwd HTTP/1.1
The resulting path resolves to /etc/passwd, leaking the server's user database. On Windows systems, ..\..\Windows\System32\config\SAM is the analogous high-value target.
Encoding bypasses are common when applications attempt naive string filtering. URL encoding (%2e%2e%2f), double encoding (%252e%252e%252f), Unicode normalization (..%c0%af), and null byte injection (file.pdf%00.jpg) can all bypass simple ../ string-match filters. The server-side URL decoder runs before the filter, effectively undoing the sanitization.
Absolute path injection is a related variant: if the application passes user input directly to a file-open function without prepending a base path, supplying /etc/passwd directly skips the need for traversal sequences entirely.
Impact
- Sensitive file disclosure —
/etc/passwd,/etc/shadow, application configuration files, private keys (~/.ssh/id_rsa), and.envfiles containing database credentials. - Source code exposure — reading application source files enables discovery of additional vulnerabilities, API keys, and business logic flaws.
- Log file poisoning — in combination with other vulnerabilities, an attacker may read log files to find injection points or write to them to falsify audit trails.
- Remote code execution — on servers where the web process can write files, traversal combined with upload functionality can write a web shell to a web-accessible path.
- Infrastructure reconnaissance — reading
/proc/net/tcp,/proc/self/environ, or cloud metadata configs reveals internal network topology and credentials.
Detection
- Fuzz all filename and path parameters — replace the value with
../../../../etc/passwd(Linux) or..\..\..\windows\win.ini(Windows). A response containing file contents confirms the vulnerability. - Test encoding variants — if the base payload is blocked, try URL-encoded (
%2e%2e%2f), double-encoded (%252e%252e%252f), and Unicode variants (..%c0%af). Use Burp Suite's Intruder with a path traversal wordlist such as the one included in SecLists (Fuzzing/LFI/LFI-Jhaddix.txt). - Check for absolute path acceptance — supply
/etc/passwddirectly (without traversal sequences) to test whether the base directory prefix is enforced at all. - Test all file-serving endpoints — download links, image parameters, template names, include directives, and log viewer endpoints. File-adjacent parameters (e.g.,
format=,template=,lang=) are frequently overlooked. - Examine responses for partial disclosure — even when a file cannot be read completely, error messages may reveal the resolved path, confirming path construction logic.
Remediation
Canonicalize before validating. Use language-native path resolution functions to get the absolute, canonical path, then verify it starts with the permitted base directory:
import os
BASE_DIR = "/var/www/uploads"
def safe_open(filename):
full_path = os.path.realpath(os.path.join(BASE_DIR, filename))
if not full_path.startswith(BASE_DIR + os.sep):
raise ValueError("Path traversal detected")
with open(full_path, "rb") as f:
return f.read()
Use indirect file references. Rather than accepting filenames from the client, accept an opaque identifier (UUID or integer) and map it server-side to a file path stored in a database. Users never interact with actual paths.
Restrict file system permissions. The web server process should run as a low-privileged user with read access limited to the directories it legitimately needs. This limits blast radius if traversal does occur.
Apply allowlist validation. If filenames must be accepted from users, validate against a strict allowlist of allowed characters (e.g., alphanumeric, dash, dot) and reject anything that does not match.
