Path Traversal Defense in Practice: From Internals to Web App Hardening
Path traversal lets attackers read arbitrary server files; it is a high-severity OWASP Top 10 issue. This guide covers a full defense at Nginx, PHP/Python/Java app, and WAF layers, with production-ready code templates and configs.
Scope
This tutorial is for web developers and security ops who need to block path traversal (Path Traversal, also called directory traversal) at both code and server layers. The flaw lets attackers escape the web root and read arbitrary files (`/etc/passwd`, configs, DB credentials). It is a common high-severity issue in OWASP Top 10.
Prerequisites
- Web server running (Nginx/Apache/OpenResty)
- App in PHP/Python/Java/Node.js
- File read or download features (image preview, attachment download, log export)
- File-op path args come from user input (GET/POST params, Cookie, request headers)
Internals
Path traversal uses special sequences in file paths to bypass access controls. Attackers inject `../` or `..\` to walk up directories and reach files outside the app's intended tree. Common techniques:
- Basic traversal: `../../../etc/passwd`
- URL-encoding bypass: `%2e%2e%2f` (URL-encoded `../`)
- Double-encoding bypass: `%252e%252e%252f` (double-encoded `%2e%2e%2f`)
- Unicode bypass: `..%c0%af` (UTF-8 encoding bypass unique to old IIS)
- Null-byte truncation: `../../../etc/passwd%00.jpg` (legacy PHP < 5.3.4)
Core rule: never trust user-supplied file paths. Enforce a strict path whitelist on every file operation.
Steps
Step 1: Block sensitive files at Nginx
Even if the app layer is vulnerable, Nginx intercepts first:
# Add inside the server block
location ~* \.(env|git|svn|swp|bak|sql|log)$ {
deny all;
return 403;
}
location ~* /\.(git|svn|hg|idea) {
deny all;
return 403;
}
# Block access to critical system files
location ~* (etc/passwd|etc/shadow|etc/hosts|boot\.ini|win\.ini) {
deny all;
return 403;
}
# Restrict PHP execution dirs (block webshells in upload dirs)
location ~ /uploads/.*\.php$ {
deny all;
return 403;
}
Add the config to `/etc/nginx/conf.d/security.conf`, then `nginx -t && systemctl reload nginx`.
Step 2: PHP app layer — canonicalize, then check the base path
// Wrong: concatenate user input directly
$file = $_GET['file'];
$content = file_get_contents('/var/www/downloads/' . $file); // ❌ Dangerous!
// Correct: canonicalize path, then check prefix
function safeReadFile(string $userInput, string $baseDir): string {
// 1. Strip null bytes and traversal sequences
$clean = str_replace(['\0', '%00', "\0"], '', $userInput);
// 2. Reject input containing ../ or ..\
if (strpos($clean, '../') !== false || strpos($clean, '..\\') !== false) {
throw new \Exception('Invalid path');
}
// 3. Canonicalize to absolute path (resolve all .. and .)
$baseReal = realpath($baseDir);
$targetReal = realpath($baseDir . '/' . $clean);
// 4. Critical check: target path must start with the base path
if ($targetReal === false || strpos($targetReal, $baseReal) !== 0) {
throw new \Exception('Path traversal detected');
}
if (!is_file($targetReal) || !is_readable($targetReal)) {
throw new \Exception('File not found or not readable');
}
return file_get_contents($targetReal);
}
// Usage
try {
$content = safeReadFile($_GET['file'], '/var/www/downloads');
echo htmlspecialchars($content);
} catch (\Exception $e) {
http_response_code(403);
echo 'Access denied';
}
Core logic: resolve user input + base dir with realpath() to an absolute path, then verify the result starts with the base dir's realpath. This blocks all `../` variants.
Step 3: Python app layer — validate with os.path.abspath
import os
BASE_DIR = '/var/www/downloads'
def safe_read_file(user_path: str) -> bytes:
# Strip traversal sequences
user_path = user_path.replace('\0', '').replace('%00', '')
# Reject explicit traversal
if '../' in user_path or '..\\' in user_path:
raise PermissionError('Invalid path')
# Build and canonicalize the full path
full_path = os.path.abspath(os.path.join(BASE_DIR, user_path))
# Verify the target stays inside the base dir
if not full_path.startswith(os.path.abspath(BASE_DIR) + os.sep):
raise PermissionError('Path traversal detected')
# Verify the file exists and is in bounds
if not os.path.isfile(full_path):
raise FileNotFoundError('File not found')
with open(full_path, 'rb') as f:
return f.read()
Same pattern: canonicalize, then prefix-check. `os.path.abspath` expands `..` so any traversal sequence becomes a real path.
Step 4: Java app layer — use CanonicalPath
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileSecurity {
private static final String BASE_DIR = "/var/www/downloads";
public static byte[] safeReadFile(String userPath) throws IOException {
// Reject explicit traversal
if (userPath.contains("../") || userPath.contains("..\\")
|| userPath.contains("\0") || userPath.contains("%00")) {
throw new SecurityException("Invalid path");
}
// Build path and get the canonical path
Path basePath = Paths.get(BASE_DIR).toRealPath();
Path targetPath = Paths.get(BASE_DIR, userPath).normalize().toRealPath();
// Verify canonical path starts with the base path
if (!targetPath.startsWith(basePath)) {
throw new SecurityException("Path traversal detected");
}
return Files.readAllBytes(targetPath);
}
}
Java `toRealPath()` and `normalize()` resolve `..` and symlinks. Safest option.
Step 5: Restrict download directory at Nginx
# Method 1: alias + directory restriction
location /downloads/ {
alias /var/www/downloads/;
# Block escape via ./..
if ($uri ~* '\.\.') {
return 403;
}
}
# Method 2: X-Accel-Redirect internal redirect (recommended)
# PHP/Python validates, then issues an internal redirect
location /protected-files/ {
internal; # Internal only; block direct external requests
alias /var/www/downloads/;
add_header Content-Disposition 'attachment';
}
X-Accel-Redirect is Nginx best practice: the app only checks auth, then sets the `X-Accel-Redirect` header; Nginx serves the file internally. Path never leaks to the user, and Nginx handles static I/O (faster).
Step 6: Block path traversal at the WAF
# Nginx + ngx_http_lua_module (OpenResty)
# In server or http block
# Method 1: regex intercept (simple, effective)
if ($args ~* "(\.\./|\.\.\\|\.\.%2f|%2e%2e%2f|%252e%252e%252f)") {
return 403;
}
# Method 2: map (faster; no if-block limits)
map $args $block_path_traversal {
default 0;
~*(\.\./|\.\.\\|\.\.%2f|%2e%2e%2f|%252e%252e%252f) 1;
~*(etc/passwd|etc/shadow|boot\.ini|win\.ini|windows/win\.ini) 1;
}
server {
if ($block_path_traversal = 1) {
return 403;
}
# ... rest of config
}
Verification
# Basic path traversal test
curl -v "https://helixcyber.ai/download?file=../../../etc/passwd"
# Expected: 403 Forbidden
# URL-encoding bypass test
curl -v "https://helixcyber.ai/download?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd"
# Expected: 403 Forbidden
# Double-encoding test
curl -v "https://helixcyber.ai/download?file=%252e%252e%252f%252e%252e%252fetc/passwd"
# Expected: 403 Forbidden (WAF and app both must block)
# Legit file download check
curl -v "https://helixcyber.ai/download?file=report_2026.pdf"
# Expected: file body or download
FAQ
Q1: Why is realpath() more reliable than regex filters?
Regex cannot enumerate every encoding variant. Attackers use double URL encoding, Unicode, and malformed paths (e.g. `....//` resolving to `../` on some systems). realpath() hands the path to the filesystem; whatever encoding wraps `../`, the prefix check still catches the canonical result. Use regex only as WAF-layer backup.
Q2: How to configure on Windows servers?
Windows uses `..\`; same defense model:
- IIS: `Request.MapPath()` then prefix-check the physical path
- ASP.NET: `Path.GetFullPath()` instead of realpath
- Windows is case-insensitive: `C:\Windows` equals `C:\windows`
- Guard against `::$DATA` ADS bypass (IIS 6 leftover)
Q3: How to block symlink bypass?
Even without `../`, an uploaded symlink pointing at `/etc` can bypass. Mitigations:
- Resolve symlinks with `realpath()` or `toRealPath()`
- On upload, reject links via `is_link()` or `lstat()`
- On Linux, `openat2()` (kernel 5.6+) with `RESOLVE_NO_SYMLINKS`
Takeaway
Path traversal needs layered defense: Nginx regex intercept, app-layer canonical path checks, WAF as backup against encoding variants. Canonicalize first, then prefix-check—do not rely on blacklists. Wrap all downloads in a safe helper and use X-Accel-Redirect for best protection.