7 min read

Intermediate

Path Traversal Remediation

Risk Severity
High
Fix Effort
Medium
Est. Time
2-4 hours
Reference
A01:2025 CWE-22

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

Successful path traversal can expose configuration files, credentials, source code, and system files. Combined with file upload vulnerabilities, it can lead to remote code execution.

Path Traversal Defense

Understanding Path Traversal

Attack Examples

Basic traversal:
../../../etc/passwd ..\..\..\..\windows\win.ini
Encoded variants:
%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 response

Common 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.

Path Traversal PortSwigger easy
Basic traversalEncoding bypassValidation bypass

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), openat with a directory descriptor, and O_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