Path Traversal Remediation
Path traversal (directory traversal) attacks allow attackers to access files outside the intended directory using sequences like "../". This can lead to reading sensitive files, source code, or even code execution.
Impact
Path Traversal Defense
Understanding Path Traversal
Attack Examples
../../../etc/passwd
..\..\..\..\windows\win.ini
%2e%2e%2f%2e%2e%2f
....//....//etc/passwd
Remediation Strategies
1. Avoid User Input in Paths
Use database IDs or UUIDs that map to files instead of filenames.
2. Canonicalize and Validate
Resolve the full path, then verify it starts with the allowed base path.
3. Allowlist Approach
Only allow specific, predefined files or patterns.
4. Chroot/Sandbox
Run file operations in a restricted directory jail.
Python
import os
from pathlib import Path
UPLOAD_DIR = Path('/var/www/uploads').resolve()
def safe_file_access(user_filename):
"""Safely access a file within the upload directory."""
# Construct the full path
requested_path = (UPLOAD_DIR / user_filename).resolve()
# Verify path is within allowed directory
if not str(requested_path).startswith(str(UPLOAD_DIR)):
raise ValueError("Access denied: path traversal attempt")
# Additional check: file must exist
if not requested_path.is_file():
raise FileNotFoundError("File not found")
return requested_path
# Alternative: Use indirect references
FILE_MAPPING = {
'doc1': '/var/www/uploads/document1.pdf',
'doc2': '/var/www/uploads/document2.pdf',
}
def get_file_by_id(file_id):
if file_id not in FILE_MAPPING:
raise ValueError("Invalid file ID")
return FILE_MAPPING[file_id]Java
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class SafeFileAccess {
private static final String BASE_DIR = "/var/www/uploads";
public static File safeGetFile(String userInput) throws IOException {
// Resolve and canonicalize the path
Path basePath = Paths.get(BASE_DIR).toRealPath();
Path requestedPath = basePath.resolve(userInput).normalize().toRealPath();
// Verify path is within base directory
if (!requestedPath.startsWith(basePath)) {
throw new SecurityException("Path traversal attempt detected");
}
return requestedPath.toFile();
}
// Alternative: Sanitize filename (remove path components)
public static String sanitizeFilename(String filename) {
return new File(filename).getName(); // Returns only the filename
}
}Node.js
const path = require('path');
const fs = require('fs');
const BASE_DIR = '/var/www/uploads';
function safeFileAccess(userFilename) {
// Resolve to absolute path
const requestedPath = path.resolve(BASE_DIR, userFilename);
// Normalize and check if within base directory
const normalizedBase = path.normalize(BASE_DIR);
const normalizedPath = path.normalize(requestedPath);
if (!normalizedPath.startsWith(normalizedBase + path.sep)) {
throw new Error('Access denied: path traversal attempt');
}
// Additional validation
if (!fs.existsSync(requestedPath)) {
throw new Error('File not found');
}
return requestedPath;
}
// Alternative: Use basename to strip directory components
function sanitizeFilename(filename) {
return path.basename(filename);
}PHP
<?php
define('BASE_DIR', '/var/www/uploads');
function safeFileAccess($userFilename) {
// Build full path and resolve it
$requestedPath = realpath(BASE_DIR . DIRECTORY_SEPARATOR . $userFilename);
$basePath = realpath(BASE_DIR);
// realpath returns false if file doesn't exist
if ($requestedPath === false) {
throw new Exception('File not found');
}
// Verify path is within base directory
if (strpos($requestedPath, $basePath) !== 0) {
throw new Exception('Access denied: path traversal attempt');
}
return $requestedPath;
}
// Alternative: Strip directory from filename
function sanitizeFilename($filename) {
return basename($filename);
}
?>Testing Verification
Path Traversal Test Payloads
# Basic traversal - should be blocked
../../../etc/passwd
..\..\..\windows\win.ini
# URL encoded
%2e%2e%2f%2e%2e%2fetc%2fpasswd
%2e%2e%5c%2e%2e%5cwindows%5cwin.ini
# Double encoding
%252e%252e%252f
# Null byte (older systems)
../../../etc/passwd%00.jpg
# Unicode/overlong encoding
..%c0%af..%c0%af
# Path normalization bypass
....//....//etc/passwd
..;/..;/etc/passwd
# Expected: All should fail or return safe responseCommon Mistakes
Blocklist Approach
# DON'T: Easy to bypass
if "../" in filename:
reject()Bypassed with encoding or ....//
Correct Approach
# DO: Resolve and verify
real_path = Path(base / file).resolve()
if not str(real_path).startswith(base):
reject()Canonicalization handles all bypasses
Checking Before Canonicalization
# DON'T: Check raw input
if not raw_path.startswith(base):
reject()
# Then use raw_path...Path may resolve differently
Correct Approach
# DO: Canonicalize first
real = os.path.realpath(raw_path)
if not real.startswith(base):
reject()Check the actual resolved path
Path Traversal Prevention Checklist
- ☐ User input not used directly in file paths
- ☐ Path canonicalization before access checks
- ☐ Resolved path verified to be within base directory
- ☐ Filename sanitization (strip directory components)
- ☐ Indirect file references used where possible
- ☐ File system permissions properly configured
Path Traversal Labs
Practice exploiting and fixing path traversal vulnerabilities.
Evidence to Capture
The traversal that worked: the parameter, the exact encoding (../, ..%2f, double-encoded, backslash) and the response containing the file contents.
File excerpt: A short excerpt with a value that only exists on the host — a user entry, a configuration section, a key header — not a blank 200 or a reflected path.
Base directory and depth: How many levels were needed and the base directory the application was reading from, from the error message or the path you finally reached.
Write case: For upload, rename or export parameters, the file created outside the intended directory and the request that created it, read back through a different feature.
WAF behaviour by variant: Which encodings the edge rejected and which passed, because that determines whether the fix belongs in code or can be deferred by a rule.
Likely False Positives
- 404 on the traversal attempt: A rejected or normalised path returning 404 is the control working. Only a response containing file bytes counts.
- Path echoed without being opened: An error message that reproduces your input proves nothing. Check whether the body contains content from the file.
- Router normalisation of a legitimate path: A URL such as /a/../b resolved by the HTTP router is not traversal; the finding requires reaching a file outside an intended directory.
- Pre-existing symlink in the application directory: If the link was created by the deployment, reaching it through a documented path is a packaging issue unless an attacker can create the link.
- Windows-specific claims without content: Short names or alternate data stream payloads that return an error are not findings. Show the file content reached.
Testing Checklist
Discovery
- ☐ Find every user-supplied path: download, preview, template or locale include, log viewer, backup restore, avatar path, plugin loader.
- ☐ Note how the path is composed — string concatenation, a join, or a resolve with a base directory — because that determines whether a canonicalisation check will hold.
- ☐ Look for parameters that accept a path in a hidden field or an API body rather than the visible UI.
Confirmation
- ☐ Send raw, single-encoded, double-encoded and absolute-path variants, plus backslashes and a null byte on older runtimes.
- ☐ Request one file that exists and one that does not, so a generic error cannot be mistaken for traversal.
- ☐ After the fix, re-send the successful payload and confirm the canonical path is rejected rather than silently normalised to a valid file.
Impact
- ☐ Read a specific sensitive file — application config, environment file, private key, credential store — and state the privilege of the process that read it.
- ☐ For a write primitive, show a file placed outside the intended directory and what consumes it: a template, a config, an SSH key path.
- ☐ Check whether the parameter can also list directories, which turns one file into a map of the host.
Remediation Summary
Structural fix
- • Stop composing filesystem paths from user input. Accept an identifier and resolve it through a server-side map to a stored path.
- • Where a relative path must be accepted, canonicalise it and verify the result is inside the intended base directory before opening, rejecting anything outside.
- • Use the runtime facilities that make the check reliable:
Path.resolve().is_relative_to(base),openatwith a directory descriptor, andO_NOFOLLOW.
Compensating control
- • Run the file-serving component in a container with only the intended directory mounted, so a bypass reaches nothing else.
- • Reject encoded separators and traversal sequences at the edge as an interim measure, accepting that it is bypassable.
- • Serve user files from object storage by generated key rather than from the filesystem, which removes the path input entirely.
CWE References: CWE-22