Path traversal (also known as directory traversal) is a web security vulnerability that allows attackers to access files and directories stored outside the intended directory on a server. By manipulating file path references, attackers can read sensitive files, access configuration data, or even execute malicious code in severe cases.
This vulnerability occurs when an application uses user-supplied input to construct file paths without proper validation or sanitization.
How Path Traversal Works
The core concept involves using special character sequences like ../ (dot-dot-slash) to navigate up the directory tree. For example:
Normal request: /var/www/files/document.pdf
Malicious request: /var/www/files/../../../../etc/passwd
The ../ sequence tells the system to move up one directory level. By chaining multiple sequences, an attacker can traverse to any accessible location on the filesystem.
Implement proper access controls and authentication
Log all file access attempts
Use security headers and WAF rules
Regular security audits and penetration testing
Keep frameworks and libraries updated
Testing for Path Traversal
Manual Testing
Automated Tools
Burp Suite
OWASP ZAP
Nikto
Custom scripts with fuzzing lists
Real-World Impact
Path traversal vulnerabilities can lead to:
Exposure of sensitive configuration files
Access to password hashes and authentication data
Reading application source code
Remote code execution (when combined with file upload)
Complete system compromise
Conclusion
Path traversal vulnerabilities remain a critical security issue despite being well-documented. Developers must always validate and sanitize user input, use proper path handling functions, and implement defense-in-depth strategies to protect against these attacks. Regular security testing and code reviews are essential to identify and remediate these vulnerabilities before they can be exploited.
from flask import Flask, request, send_file
import os
app = Flask(__name__)
# VULNERABLE CODE - DO NOT USE
@app.route('/download')
def download_file():
filename = request.args.get('file')
file_path = os.path.join('/var/www/uploads/', filename)
return send_file(file_path)
# Attack: /download?file=../../../etc/passwd
from flask import Flask, request, send_file, abort
import os
app = Flask(__name__)
@app.route('/download')
def download_file():
filename = request.args.get('file')
# Normalize and validate the path
base_directory = os.path.abspath('/var/www/uploads/')
requested_path = os.path.abspath(os.path.join(base_directory, filename))
# Ensure the requested file is within the allowed directory
if not requested_path.startswith(base_directory):
abort(403)
# Check if file exists
if not os.path.exists(requested_path):
abort(404)
return send_file(requested_path)
<?php
// VULNERABLE CODE - DO NOT USE
$file = $_GET['file'];
$content = file_get_contents("/var/www/documents/" . $file);
echo $content;
// Attack: ?file=../../../etc/passwd
?>
<?php
$file = $_GET['file'];
$baseDir = '/var/www/documents/';
// Remove any path traversal sequences
$file = str_replace(['../', '..\\'], '', $file);
// Get the real path and verify it's within the base directory
$fullPath = realpath($baseDir . $file);
if ($fullPath === false || strpos($fullPath, realpath($baseDir)) !== 0) {
die('Access denied');
}
if (file_exists($fullPath)) {
$content = file_get_contents($fullPath);
echo $content;
} else {
die('File not found');
}
?>
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
app.get('/files/:filename', (req, res) => {
const filename = req.params.filename;
const baseDir = path.resolve('./uploads');
// Resolve the full path
const filePath = path.resolve(baseDir, filename);
// Verify the resolved path is within the base directory
if (!filePath.startsWith(baseDir)) {
return res.status(403).send('Access denied');
}
fs.readFile(filePath, (err, data) => {
if (err) return res.status(404).send('Not found');
res.send(data);
});
});
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RestController
public class FileController {
// VULNERABLE CODE - DO NOT USE
@GetMapping("/download")
public byte[] downloadFile(@RequestParam String filename) throws Exception {
File file = new File("/var/www/files/" + filename);
return Files.readAllBytes(file.toPath());
}
// Attack: /download?filename=../../../etc/passwd
}
import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RestController
public class FileController {
private static final String BASE_DIR = "/var/www/files/";
@GetMapping("/download")
public byte[] downloadFile(@RequestParam String filename) throws IOException {
// Normalize the path
Path basePath = Paths.get(BASE_DIR).toAbsolutePath().normalize();
Path filePath = basePath.resolve(filename).normalize();
// Ensure the file is within the allowed directory
if (!filePath.startsWith(basePath)) {
throw new ResponseStatusException(
HttpStatus.FORBIDDEN, "Access denied"
);
}
// Check if file exists and is readable
if (!Files.exists(filePath) || !Files.isReadable(filePath)) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "File not found"
);
}
return Files.readAllBytes(filePath);
}
}
# VULNERABLE CODE - DO NOT USE
class FilesController < ApplicationController
def download
filename = params[:file]
file_path = Rails.root.join('uploads', filename)
send_file file_path
end
end
# Attack: /download?file=../../../etc/passwd
class FilesController < ApplicationController
def download
filename = params[:file]
base_path = Rails.root.join('uploads').expand_path
file_path = base_path.join(filename).expand_path
# Verify the file is within the allowed directory
unless file_path.to_s.start_with?(base_path.to_s)
render plain: 'Access denied', status: :forbidden
return
end
# Check if file exists
unless File.exist?(file_path)
render plain: 'File not found', status: :not_found
return
end
send_file file_path
end
end
using Microsoft.AspNetCore.Mvc;
using System.IO;
// VULNERABLE CODE - DO NOT USE
[ApiController]
[Route("[controller]")]
public class FileController : ControllerBase
{
[HttpGet("download")]
public IActionResult DownloadFile(string filename)
{
var filePath = Path.Combine(@"C:\uploads\", filename);
var bytes = System.IO.File.ReadAllBytes(filePath);
return File(bytes, "application/octet-stream", filename);
}
// Attack: /file/download?filename=..\..\..\Windows\System32\config\sam
}
using Microsoft.AspNetCore.Mvc;
using System.IO;
[ApiController]
[Route("[controller]")]
public class FileController : ControllerBase
{
private readonly string _baseDirectory = @"C:\uploads\";
[HttpGet("download")]
public IActionResult DownloadFile(string filename)
{
// Get the full path
var baseDir = Path.GetFullPath(_baseDirectory);
var fullPath = Path.GetFullPath(Path.Combine(baseDir, filename));
// Ensure the file is within the allowed directory
if (!fullPath.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase))
{
return StatusCode(403, "Access denied");
}
// Check if file exists
if (!System.IO.File.Exists(fullPath))
{
return NotFound("File not found");
}
var bytes = System.IO.File.ReadAllBytes(fullPath);
return File(bytes, "application/octet-stream", Path.GetFileName(fullPath));
}
}