Path Traversal
Path Traversal (Directory Traversal) allows attackers to access files outside the intended directory by manipulating file path inputs with sequences like ../. Learn how to detect and prevent path traversal across languages.
What is Path Traversal?
Path Traversal (CWE-22), also known as Directory Traversal, is a vulnerability that allows an attacker to read, write, or delete files outside the application’s intended directory. It occurs when an application uses user-supplied input to construct file paths without adequately validating or sanitizing the path components.
The classic technique uses ../ (dot-dot-slash) sequences to navigate up the directory tree:
GET /download?file=../../../etc/passwd
Path traversal can be exploited to:
- Read sensitive files — Configuration files, source code, private keys,
/etc/passwd,/etc/shadow - Read application secrets — Database credentials in config files, API keys in
.envfiles - Write malicious files — Overwrite configuration, plant web shells, modify log files
- Achieve remote code execution — By writing executable files to web-accessible directories or overwriting application code
Why it matters
Path traversal is ranked under OWASP A01:2021 Broken Access Control, the number-one risk category. Its impact is significant because:
- Direct access to sensitive data — Attackers can read database connection strings, private keys, and other secrets stored on the filesystem
- Common in file-serving applications — Any feature that serves files based on user input (document downloads, avatar uploads, report generation) is at risk
- Often leads to full compromise — Reading SSH keys, AWS credentials, or application configs frequently enables complete infrastructure takeover
- Encoding bypasses are common — Simple blocklist-based defenses are routinely bypassed using URL encoding (
%2e%2e%2f), double encoding, null bytes, or Unicode normalization - Zip slip attacks — Archive extraction operations that don’t validate member paths can write files to arbitrary locations
How exploitation works
Basic directory traversal
An application serves user-uploaded documents:
GET /api/documents?name=report.pdf
The server constructs the path as /var/app/uploads/report.pdf. An attacker modifies the request:
GET /api/documents?name=../../../etc/passwd
The server resolves /var/app/uploads/../../../etc/passwd to /etc/passwd and returns its contents.
Encoding bypasses
Attackers use various encodings to evade filters:
# URL encoding
GET /api/documents?name=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
# Double URL encoding
GET /api/documents?name=%252e%252e%252f%252e%252e%252fetc%252fpasswd
# Null byte injection (older runtimes)
GET /api/documents?name=../../../etc/passwd%00.pdf
# Backslash on Windows
GET /api/documents?name=..\..\..\..\windows\win.ini
# Mixed separators
GET /api/documents?name=..%5c..%5c..%5cwindows%5cwin.ini
Zip slip
When extracting archives, a malicious zip can contain entries with traversal paths:
../../../../tmp/evil.sh
../../../var/www/html/shell.php
If the application extracts these without validating member paths, files are written to arbitrary locations.
Vulnerable code examples
C# / ASP.NET
// VULNERABLE: User input directly used in file path
[HttpGet("download")]
public IActionResult Download(string filename)
{
string filePath = Path.Combine("/var/app/uploads", filename);
return PhysicalFile(filePath, "application/octet-stream");
}
Java / Spring
// VULNERABLE: No path validation
@GetMapping("/files")
public ResponseEntity<Resource> getFile(@RequestParam String name) {
Path filePath = Paths.get("/var/app/uploads/" + name);
Resource resource = new FileSystemResource(filePath.toFile());
return ResponseEntity.ok().body(resource);
}
Python / Django
# VULNERABLE: User-controlled path with no validation
def download_file(request):
filename = request.GET.get('file')
filepath = os.path.join('/var/app/uploads', filename)
with open(filepath, 'rb') as f:
return HttpResponse(f.read(), content_type='application/octet-stream')
Node.js / Express
// VULNERABLE: path.join does NOT prevent traversal
app.get('/files', (req, res) => {
const filename = req.query.name;
const filePath = path.join(__dirname, 'uploads', filename);
res.sendFile(filePath);
});
Go
// VULNERABLE: Direct concatenation with user input
func downloadHandler(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("file")
filepath := "/var/app/uploads/" + filename
http.ServeFile(w, r, filepath)
}
Secure code examples
C# — Canonicalize and validate
// SECURE: Resolve canonical path and verify it stays within the base directory
[HttpGet("download")]
public IActionResult Download(string filename)
{
string basePath = Path.GetFullPath("/var/app/uploads");
string filePath = Path.GetFullPath(Path.Combine(basePath, filename));
// Ensure the resolved path starts with the base directory
if (!filePath.StartsWith(basePath + Path.DirectorySeparatorChar))
{
return BadRequest("Invalid filename");
}
if (!System.IO.File.Exists(filePath))
return NotFound();
return PhysicalFile(filePath, "application/octet-stream");
}
Java — Canonical path validation
// SECURE: Canonical path comparison
@GetMapping("/files")
public ResponseEntity<Resource> getFile(@RequestParam String name) throws IOException {
File baseDir = new File("/var/app/uploads").getCanonicalFile();
File requestedFile = new File(baseDir, name).getCanonicalFile();
// Verify the canonical path is within the base directory
if (!requestedFile.toPath().startsWith(baseDir.toPath())) {
return ResponseEntity.badRequest().build();
}
Resource resource = new FileSystemResource(requestedFile);
return ResponseEntity.ok().body(resource);
}
Python — Resolve and check
# SECURE: Resolve symlinks and verify path prefix
from pathlib import Path
def download_file(request):
filename = request.GET.get('file')
base_dir = Path('/var/app/uploads').resolve()
file_path = (base_dir / filename).resolve()
# Ensure resolved path is within base directory
if not str(file_path).startswith(str(base_dir) + '/'):
return HttpResponseBadRequest("Invalid filename")
if not file_path.is_file():
return HttpResponseNotFound()
with open(file_path, 'rb') as f:
return HttpResponse(f.read(), content_type='application/octet-stream')
Node.js — Resolve and verify
// SECURE: Resolve absolute path and verify prefix
const path = require('path');
const fs = require('fs');
app.get('/files', (req, res) => {
const filename = req.query.name;
const baseDir = path.resolve(__dirname, 'uploads');
const filePath = path.resolve(baseDir, filename);
// Ensure the resolved path is within the uploads directory
if (!filePath.startsWith(baseDir + path.sep)) {
return res.status(400).send('Invalid filename');
}
if (!fs.existsSync(filePath)) {
return res.status(404).send('Not found');
}
res.sendFile(filePath);
});
Go — Clean and verify
// SECURE: filepath.Clean + prefix check
func downloadHandler(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("file")
baseDir := "/var/app/uploads"
// Clean the path to remove ../ sequences, then verify prefix
cleanPath := filepath.Join(baseDir, filepath.Clean("/"+filename))
if !strings.HasPrefix(cleanPath, baseDir+string(filepath.Separator)) {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
http.ServeFile(w, r, cleanPath)
}
What Offensive360 detects
Our SAST engine traces user-controlled input to file system operations and identifies unsafe path construction. We detect:
- Direct path concatenation — User input concatenated or interpolated into file paths without canonicalization
- Ineffective sanitization — Simple string replacement of
../(which can be bypassed with....//or encoding tricks) - path.join / Path.Combine misuse — These functions combine paths but do not prevent traversal; we flag when they are used without subsequent canonical path validation
- Zip/archive extraction — Archive member paths not validated against a base directory during extraction (Zip Slip)
- File upload destination — User-controlled filenames used to determine upload storage paths
- Symlink following — File operations that follow symbolic links without restricting the target to an allowed directory
Each finding provides the full data-flow trace and identifies the specific file operation (read, write, delete, include) that is reachable.
Remediation guidance
-
Canonicalize then validate — Always resolve the final absolute path using
Path.GetFullPath(),getCanonicalFile(),Path.resolve(), or equivalent, and then verify the result starts with your expected base directory. This is the most reliable defense. -
Never trust user input for filenames — If possible, use an indirect reference (e.g., a database ID that maps to a filename) instead of accepting filenames directly from users.
-
Strip path separators — Remove or reject input containing
/,\,.., null bytes, and URL-encoded variants. But do this as defense-in-depth, not as your sole protection. -
Use a chroot or sandboxed directory — Configure the application or container so that the process literally cannot access files outside its designated directory.
-
Validate archive entries — When extracting zip/tar archives, check that each entry’s path resolves to a location within the target directory before writing.
-
Disable directory listing — Ensure your web server does not expose directory indexes, which help attackers discover files to target.
False-positive considerations
Offensive360 may flag file operations that are safe in certain contexts:
- Static/constant paths — If the file path is entirely hardcoded and no user input influences it, there is no traversal risk
- Integer-based lookups — If user input is parsed to an integer and used to look up a filename from a database or map
- Canonical path validation present — If the code resolves the path and checks the prefix, but the analysis doesn’t recognize the custom validation function
- Containerized environments — Even if traversal occurs, a container’s filesystem may not contain sensitive files; however, this is not a reliable mitigation
References
Related vulnerabilities
Detect Path Traversal in your code
Run Offensive360 SAST against your codebase to find this and hundreds of other vulnerabilities.