Path traversal (CWE-22) lets attackers escape the intended directory with ../ sequences to read sensitive files, write arbitrary content, or achieve code execution.
TL;DR
../, encoded, null byte, absolute path injection, Windows ..\\realpath after join, then startsWith(base + sep) — denylist filters always failPath traversal (CWE-22) is a vulnerability in which an application constructs a filesystem path from user-controlled input without adequately constraining where that path can resolve. The attacker inserts ../ sequences — or their encoded equivalents — to navigate upward past the intended base directory, reaching files the application never intended to expose. Under OWASP A01:2021 (Broken Access Control), it represents a failure to enforce least privilege at the filesystem level.
The vulnerability appears wherever server-side code constructs a path from external input: file download endpoints, template loaders, image servers, log viewers, document generators, and plugin installers. An 85% year-over-year increase in closed-source project prevalence (1.9% to 3.5% of all findings) from Aikido's 2024 analysis shows the attack surface is growing. Multiple CISA KEV entries in 2024 confirm active exploitation at scale.
Path traversal is distinct from SSRF, which manipulates URL resolution to reach internal network services. The two primitives sometimes combine: urllib.parse.urljoin(base, "/etc/passwd") silently ignores the base when the user input begins with /, creating a path traversal primitive inside URL-fetching code (CVE-2026-32871, FastMCP, CVSS 9.8).
The vulnerability has a single structural root cause: an application concatenates a fixed base directory with user input and passes the result to a filesystem function without first verifying that the resolved canonical path remains within the base.
Five exploitation steps:
file=, path=, template=, page=, include=, doc=, resource=.../../etc/passwd)... segments, escaping the intended directory.Confirmation markers differ by target. /etc/passwd returns lines matching root:x:0:0:. /proc/self/environ exposes runtime environment variables that frequently contain AWS_SECRET_ACCESS_KEY, DATABASE_URL, and API_KEY values. WEB-INF/web.xml returns XML containing <web-app.
| Variant | Technique | Key CVEs | Impact |
|---|---|---|---|
Basic ../ Traversal | Literal dot-dot-slash chains | CVE-2024-23897, CVE-2023-2825 | Arbitrary file read, LFI to RCE |
| Encoded Traversal | URL, double, Unicode overlong encoding | CVE-2024-38819, CVE-2024-38474 | Filter and WAF bypass |
| Null Byte Injection | %00 terminates extension suffix at C layer | PHP pre-5.3.4, Java legacy | Extension filter bypass |
| Absolute Path Injection | Direct /etc/passwd bypasses ../ filters | CVE-2026-32871, CVE-2023-50164 | Python os.path.join overwrite |
| Windows Path Traversal | ..\, %5c, UNC, ADS, 8.3 names | CVE-2024-4577, CVE-2024-50379 | IIS file read, webshell via TOCTOU |
Three escalation chains consistently convert path traversal reads into remote code execution:
Log poisoning: Inject PHP code into server access logs via a crafted User-Agent: <?php system($_GET['cmd']); ?> request. When the log file is subsequently included via LFI, the PHP interpreter executes the injected payload. Target logs: /var/log/apache2/access.log, /var/log/nginx/access.log, /var/log/auth.log, /proc/self/fd/2.
PHP filter chains: php://filter/convert.base64-encode/resource=index.php returns base64-encoded PHP source code without requiring any write access. Advanced filter chain techniques (Charles Fol's php_filter_chain_generator) construct arbitrary PHP payloads targeting /dev/null, achieving unauthenticated RCE from LFI with no file upload required.
ZIP Slip: Malicious archives contain entries with traversal sequences in filenames (../../var/www/html/shell.php). Naive extraction writes the webshell into the document root. CVE-2024-1708 (ConnectWise ScreenConnect, CVSS 8.4) chained with auth bypass CVE-2024-1709 (CVSS 10.0) to achieve unauthenticated RCE as SYSTEM within 24 hours of disclosure.
CVE-2024-23897 — Jenkins CLI Parser (CVSS 9.8, CISA KEV): The args4j library's expandAtFiles() replaced @/path/to/file tokens in CLI commands with file contents. Unauthenticated attackers read /var/jenkins_home/secrets/master.key and /etc/passwd via HTTP, WebSocket, or SSH. Exploited by the RansomEXX group to compromise India's National Payments Corporation, shutting down 300+ banks. CISA mandatory patch deadline: September 9, 2024.
CVE-2024-1708 — ConnectWise ScreenConnect ZIP Slip (CVSS 8.4): Combined with auth bypass CVE-2024-1709 (CVSS 10.0), this allowed unauthenticated attackers to write arbitrary ASPX webshells to the ScreenConnect web root. Multiple ransomware groups weaponized the chain within 24 hours of disclosure, achieving SYSTEM access.
CVE-2023-2825 — GitLab Unauthenticated Path Traversal (CVSS 10.0): Arbitrary file read via path traversal in public nested project URL handling in GitLab CE/EE. No authentication required. Exposed /etc/passwd, SSH private keys, and GitLab runner registration tokens.
file=, path=, page=, template=, include=, doc=, resource=, lang=, layout=, theme=, download=.../../etc/passwd (Linux) or ..\..\Windows\win.ini (Windows) and examine the response body for confirmation markers...%2f..%2fetc%2fpasswd, %2e%2e%2f%2e%2e%2fetc%2fpasswd, ..%252f..%252fetc%252fpasswd./etc/passwd directly.../../etc/passwd%00.txt — if the response differs, a null byte bypass is present.../WEB-INF/web.xml — marker is <web-app.root:x:0:0: (passwd), [fonts] (win.ini), <web-app (web.xml).BreachVex detects path traversal through multiple complementary techniques: template-based scanning with lfi/traversal templates, parameter fuzzing across a tiered set of LFI-prone parameter names, direct HTTP verification with 22 payload variants against OS-specific content markers, and an extended LFI prover with a three-criterion content gate (HTTP 200 + body length ≥ 10 bytes + marker regex match). Fuzzing hits without content-gate confirmation are demoted to low-severity potential findings to suppress false positives.
import os
BASE_DIR = os.path.realpath("/var/www/app/uploads")
def safe_open(filename: str):
# realpath resolves all .., symlinks, and percent-encoding
requested = os.path.realpath(os.path.join(BASE_DIR, filename))
# Trailing sep prevents /uploads_backup passing the /uploads check
if not requested.startswith(BASE_DIR + os.sep):
raise ValueError("Path traversal attempt blocked")
return open(requested, "rb")// Java — getCanonicalFile resolves .. and symlinks
File base = new File("/app/uploads").getCanonicalFile();
File requested = new File(base, userInput).getCanonicalFile();
if (!requested.toPath().startsWith(base.toPath())) {
throw new SecurityException("Path traversal attempt blocked");
}The most robust defense eliminates user-controlled filenames entirely. Map opaque identifiers to real file paths in server-side code:
DOCUMENT_MAP = {
"q1_report": "/app/reports/2025_q1.pdf",
"q2_report": "/app/reports/2025_q2.pdf",
}
def get_document(key: str):
path = DOCUMENT_MAP.get(key)
if path is None:
raise ValueError("Unknown document key")
return open(path, "rb")Never implement path traversal defense using denylist filtering. Every known encoding of ../ has documented bypasses: ....// defeats strip-once filters, %252e%252e%252f defeats single-decode filters, and ..%c0%af exploits overlong UTF-8 decoders. The only correct approach is canonical path resolution followed by a base prefix assertion.
Deploy chroot jails, AppArmor/SELinux mandatory access control, or read-only container filesystems to limit blast radius. Even when path validation fails, a properly contained process cannot access files outside its restricted filesystem view.
../ sequences, Linux file targets, and the LFI-to-RCE log poisoning chain./etc/passwd payloads and Python os.path.join behavior.%00 extension filter bypass in PHP, Java, and Go...\, UNC paths, Alternate Data Streams, and CVE-2024-4577.file:// or urljoin quirks.Path traversal (CWE-22) is a vulnerability where an application constructs a filesystem path from user-controlled input without proper validation. Attackers insert sequences like ../ to escape the intended base directory and access arbitrary files on the server, including configuration files, private keys, and credentials.
Path traversal is the broader class: the attacker reads or writes files outside an intended directory. Local File Inclusion (LFI) is a PHP-specific variant where the traversal reaches include() or require(), causing the file to execute as PHP code rather than just being read. All LFI is path traversal, but not all path traversal is LFI.
Path traversal maps to OWASP A01:2021 — Broken Access Control. CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) is the canonical weakness identifier used in CVE descriptions.
Path traversal represents 2.75% of all open-source vulnerabilities and 3.5% of closed-source findings — an 85% increase in closed-source project prevalence per Aikido research. Multiple CVSS 9.8+ CVEs were actively exploited in 2024, including CVE-2024-23897 (Jenkins, CISA KEV) and CVE-2024-1708 (ConnectWise ScreenConnect).
On Linux: /etc/passwd, /etc/shadow, /proc/self/environ (environment variables with secrets), ~/.ssh/id_rsa, ~/.aws/credentials, and application .env files. On Windows: C:\Windows\win.ini, C:\inetpub\wwwroot\web.config, and SAM backup files. In Java: WEB-INF/web.xml and application.properties.
Yes, through several chains: LFI plus log poisoning injects PHP code into Apache/Nginx access logs via User-Agent then includes the log; PHP filter chains generate arbitrary PHP payloads without file upload; ZIP Slip extracts a webshell outside the intended directory. CVE-2024-1708 (ScreenConnect) achieved unauthenticated SYSTEM access via ZIP Slip.
Filters that block literal ../ often miss encoding variants. The sequence ../ can be expressed as %2e%2e%2f (URL encoding), %252e%252e%252f (double encoding), ..%c0%af (overlong UTF-8), or ....// (nested sequence that becomes ../ after one-pass strip). Each decode cycle in the processing pipeline is a potential bypass opportunity.
Resolve the canonical path after joining the user input with the base directory, then assert it still starts with the base plus the path separator. In Python: os.path.realpath(os.path.join(BASE, user_input)), then check startswith(BASE + os.sep). In Java: new File(base, input).getCanonicalFile(), then check toPath().startsWith(base.toPath()). Denylist filters always fail.