4.10 Review Path Traversal
4.10 - Review Path Traversal
Path traversal appears when user input influences file paths without canonicalization and base-directory checks. Start from download endpoints, avatar servers, log viewers, import/export features, and archive extractors. Trace each filename from request to filesystem API.
What This Vulnerability Is
Path traversal (directory traversal) is a filesystem access flaw. Functions that open, read, write, or delete files concatenate attacker-controlled names with a base directory. Sequences like ../ or absolute paths escape the intended folder and reach sensitive files elsewhere on the server.
The unsafe assumption is that users only request files they own. This maps to CWE-22 (Improper Limitation of a Pathname to a Restricted Directory).
Vulnerability Characteristics (Where to Identify Them)
| Signal | Where to look |
|---|---|
| Feature type | File download, avatar serve, log tail, attachment read, ZIP/tar extract, backup restore |
| Input entry | Filename parameters, path segments, attachment IDs mapped to paths, archive entry names |
| Path construction | base + filename, f-strings, Path.join, send_file, http.ServeFile |
| Weak controls | Denylist of .. only, no canonical prefix check, URL-encoded traversal variants |
| Write/delete paths | Upload overwrite, extract-all without per-entry validation (zip slip) |
| Indirect paths | Database-stored filenames, object storage keys, cache keys resolved to filesystem paths |
Attack Payloads
Use these in authorized tests when a parameter influences filesystem paths. Replace FILE with the expected filename parameter (e.g., document.pdf).
Pattern 1: Basic parent-directory traversal
AVATAR=../../../etc/shadow
AVATAR=....//....//var/log/auth.log
AVATAR=..\\..\\..\\windows\\system32\\config\\sam
Pattern 2: URL-encoded and double-encoded sequences
AVATAR=..%2f..%2f..%2fvar%2flog%2fnginx%2faccess.log
AVATAR=..%252f..%252f..%252fetc%252fshadow
AVATAR=%2e%2e%2f%2e%2e%2fetc%2fhostname
Pattern 3: Absolute path bypass
AVATAR=/var/log/app.log
AVATAR=C:\inetpub\logs\LogFiles\W3SVC1\u_ex.log
AVATAR=file:///etc/hosts
Pattern 4: Null byte truncation (legacy)
AVATAR=../../../etc/passwd%00.png
AVATAR=backup.sql%00.jpg
Pattern 5: Archive entry names (zip slip)
../../../../home/deploy/.ssh/authorized_keys
..\\..\\..\\Startup\\malware.bat
Pattern 6: Unicode and normalization bypass
AVATAR=..%c0%af..%c0%afetc/passwd
AVATAR=....\/....\/etc/hosts
AVATAR=..%ef%bc%8f..%ef%bc%8fvar/log/syslog
Language-Specific Sinks and Dangerous APIs
Search for path concatenation without canonicalization and base-directory checks. Any API that opens files from user-influenced strings is a review priority.
Python
open(f"/var/www/avatars/{user_id}/{filename}")
send_file(os.path.join(AVATAR_ROOT, avatar_name))
Path(log_dir) / request.args.get("name")
shutil.copy(user_path, dest)
tarfile.extractall(user_upload) # no per-entry validation
Java
new FileInputStream(baseDir + "/" + filename);
Paths.get(uploadRoot, userSuppliedName);
Files.readAllBytes(Paths.get(userPath));
new File(base, URLDecoder.decode(name, "UTF-8"));
C
var path = Path.Combine(baseDir, filename);
File.ReadAllText(path);
File.OpenRead(userSuppliedPath);
context.Response.TransmitFile(base + "\\" + name);
JavaScript (Node.js)
fs.readFileSync(path.join(baseDir, req.query.file));
res.sendFile(path.resolve(uploads, filename));
fs.createReadStream(`/data/${req.params.name}`);
Go
http.ServeFile(w, r, filepath.Join(root, r.URL.Query().Get("f")))
ioutil.ReadFile(base + "/" + filename)
os.Open(filepath.Clean(userPath)) // Clean alone is insufficient
Shell
cat "$UPLOAD_DIR/$filename"
cp "$user_file" /var/www/
unzip "$archive" # extracts all paths without validation
C
snprintf(path, sizeof(path), "%s/%s", base, user_file);
fopen(path, "r");
open(full_path, O_RDONLY);
Sample Vulnerable Code in Python
from flask import Flask, request, send_file
app = Flask(__name__)
@app.route("/avatar")
def serve_avatar():
# Attacker-controlled avatar filename — may contain ../ sequences
avatar = request.args.get("name")
user_id = request.args.get("uid")
# Sink: path built without canonicalization or root check
return send_file(f"/var/www/avatars/{user_id}/{avatar}")
Step-by-Step Review Walkthrough
- Find file I/O endpoints. Search for download, upload, delete, and archive extract handlers that accept names or paths.
- Trace the Python (or equivalent) input path. In the sample,
avataris concatenated into a user-specific path. Ask whether../../etc/passwdresolves outside/var/www/avatars/{user_id}. - Inspect normalization. Check for
resolve(),getCanonicalPath(),filepath.Clean(), and whether results are compared to a trusted root prefix. - Review weak filters. Blocking only
..substring may miss....//, URL encoding, Unicode separators, or absolute paths. - Follow indirect paths. Database-stored filenames and attachment IDs mapped to paths need the same root check.
- Inspect write and extract operations. Traversal on upload or
extractallcan overwrite binaries or drop web shells. - Check symlink behavior. Resolved paths that follow symlinks may escape the intended directory.
Risk Impact Analysis
Sensitive file read. Attackers retrieve application secrets, source code, credentials, and system files such as /etc/passwd.
Arbitrary file write. Traversal combined with upload or extract may overwrite configuration or plant executable content in web-served directories.
Service disruption. Deleting or corrupting files outside the intended directory can break the application or host.
Compliance exposure. Unauthorized access to customer data files may trigger breach notification and audit findings.
Vulnerable Examples in Other Languages
Java
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String logName = req.getParameter("log");
String basePath = req.getServletContext().getRealPath("logs");
Path path = Paths.get(basePath, logName);
File file = path.toAbsolutePath().toFile();
if (!file.exists()) {
resp.setStatus(404);
return;
}
try (InputStream in = new FileInputStream(file)) {
IOUtils.copy(in, resp.getOutputStream());
}
}
C
[HttpGet("invoices/{id}/pdf")]
public IActionResult DownloadInvoicePdf(int id, string template)
{
var path = Path.Combine(_invoiceRoot, id.ToString(), template);
if (!System.IO.File.Exists(path))
return NotFound();
var bytes = System.IO.File.ReadAllBytes(path);
return File(bytes, "application/pdf", Path.GetFileName(path));
}
Go
func serveAvatar(w http.ResponseWriter, r *http.Request) {
avatar := r.URL.Query().Get("name")
uid := r.URL.Query().Get("uid")
path := filepath.Join("/var/www/avatars", uid, avatar)
http.ServeFile(w, r, path)
}
Fix: Safer Patterns and Libraries to Use
Python
Resolve paths and verify they stay under the upload root. Prefer framework helpers.
from pathlib import Path
from flask import send_from_directory
AVATAR_ROOT = Path("/var/www/avatars").resolve()
@app.route("/avatar")
def serve_avatar():
avatar = request.args.get("name", "")
uid = request.args.get("uid", "")
safe_path = (AVATAR_ROOT / uid / Path(avatar).name).resolve()
if not safe_path.is_relative_to(AVATAR_ROOT):
return "Forbidden", 403
if not safe_path.is_file():
return "Not found", 404
return send_from_directory(safe_path.parent, safe_path.name)
from werkzeug.security import safe_join
path = safe_join("/var/www/uploads", filename)
if path is None:
return "Invalid path", 400
Important: Use opaque stored filenames (UUIDs) on disk. Keep original names in metadata only.
Java
Normalize and verify the resolved path starts with the base directory.
Path base = Paths.get("/var/www/avatars").toAbsolutePath().normalize();
Path resolved = base.resolve(uid).resolve(Paths.get(avatar).getFileName()).normalize();
if (!resolved.startsWith(base) || !Files.isRegularFile(resolved)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
Files.copy(resolved, response.getOutputStream());
Important: Paths.get(base, filename) alone is insufficient. Always normalize and compare prefix against the trusted root.
C
Use Path.GetFullPath with a prefix check. Strip directory segments from user input.
var safeName = Path.GetFileName(template);
var fullPath = Path.GetFullPath(Path.Combine(_invoiceRoot, id.ToString(), safeName));
if (!fullPath.StartsWith(_invoiceRoot, StringComparison.OrdinalIgnoreCase))
return Forbid();
if (!System.IO.File.Exists(fullPath))
return NotFound();
return PhysicalFile(fullPath, "application/pdf", safeName);
Important: Reject rooted paths. Path.IsPathRooted(userInput) should fail for untrusted filenames.
Go
Clean paths and verify prefix under root. Prefer http.Dir or embed.FS.
func serveAvatar(w http.ResponseWriter, r *http.Request) {
name := filepath.Base(r.URL.Query().Get("name"))
uid := filepath.Base(r.URL.Query().Get("uid"))
root := filepath.Join("/var/www/avatars", uid)
clean := filepath.Clean(filepath.Join(root, name))
if !strings.HasPrefix(clean, root+string(os.PathSeparator)) && clean != root {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
http.ServeFile(w, r, clean)
}
// Zip slip protection:
dest := filepath.Clean(extractRoot)
target := filepath.Clean(filepath.Join(dest, f.Name))
if !strings.HasPrefix(target, dest+string(os.PathSeparator)) {
return fmt.Errorf("illegal path in archive")
}
Important: Validate every archive member path before extraction, not only the top-level filename.
Verify During Review
- Resolved filesystem paths are verified to stay within the intended base directory before I/O.
- User input never supplies absolute paths; directory separators and
..sequences are rejected or stripped safely. - Archive extraction validates every member path against the destination root (zip slip prevention).
- Download endpoints use framework helpers (
send_from_directory, rooted file providers) where available. - Stored filenames on disk are opaque identifiers, not raw client-provided names with path components.
- Suspicious traversal attempts are logged and covered by automated security tests.