On this page
Path Traversal in AI-Generated Code: How Your File Download Endpoint Gets Hacked
AI assistants build file-serving endpoints with string concatenation that attackers exploit with ../. Here are the vulnerable patterns your AI generates, the encoding tricks that bypass naive fixes, and the one correct solution.
Quick answer
- AI assistants build file-download endpoints with string concatenation:
open("/uploads/" + filename).read().- A request for
../../../../etc/passwdwalks up out of the uploads directory and reads system files.- Blacklisting
../doesn’t work — encoding tricks bypass it. The fix: resolve the absolute path and verify it stays in bounds.
The AI-generated pattern (vulnerable)
When you ask an AI assistant to “add a file download endpoint,” it almost always generates something like this:
# DO NOT USE — vulnerable to path traversal
@app.get("/download")
def download_file(filename: str):
filepath = os.path.join("/var/uploads", filename)
return FileResponse(filepath)// DO NOT USE — vulnerable to path traversal
app.get("/download", (req, res) => {
const filepath = path.join("/var/uploads", req.query.filename);
res.sendFile(filepath);
});Both are vulnerable to the same attack:
# Normal request:
GET /download?filename=report.pdf
# Reads: /var/uploads/report.pdf ✓
# Attack request:
GET /download?filename=../../../../etc/passwd
# Reads: /var/uploads/../../../../etc/passwd → /etc/passwd ✗The attacker walks up out of /var/uploads using ../ sequences and reads any file the server process can access.
Why naive fixes don’t work
The natural first fix — strip or reject ../ — fails because there are many ways to encode it:
# These all decode to ../ and bypass string filters:
..%2f..
%2e%2e%2f%2e%2e%2fetc%2fpasswd
..%252f..%252fetc%252fpasswd # double encoding
..%c0%af..%c0%afetc/passwd # Unicode overlong encoding
..\..\..\windows\system32\config # Windows backslashesA blacklist also fails against absolute paths: /etc/passwd contains no ../ but reads an arbitrary system file.
The only reliable fix is whitelisting by construction: resolve the final absolute path and verify it starts with the allowed base directory.
The correct fix
from pathlib import Path
BASE = Path("/var/uploads").resolve()
def safe_file_path(filename: str) -> Path:
# Resolve the final absolute path (e.g., /var/uploads/../../etc/passwd → /etc/passwd)
resolved = (BASE / filename).resolve()
# Verify it stays inside the allowed directory
if not resolved.is_relative_to(BASE):
raise ValueError("path escapes base directory")
return resolved
@app.get("/download")
def download_file(filename: str):
try:
filepath = safe_file_path(filename)
return FileResponse(filepath)
except ValueError:
raise HTTPException(status_code=404)const path = require("path");
const BASE = path.resolve("/var/uploads");
function safePath(filename) {
const resolved = path.resolve(BASE, filename);
if (!resolved.startsWith(BASE + path.sep)) {
throw new Error("path escapes base directory");
}
return resolved;
}The key operations:
- Resolve the full path first — this normalizes all
../sequences - Verify the result starts with the base directory
- Reject anything that escapes
The even simpler fix: don’t take filenames at all
If you can avoid user-supplied filenames entirely, do it. Use an ID-based lookup:
# Instead of: GET /download?filename=report.pdf
# Use: GET /download/42
@app.get("/download/{file_id}")
def download_file(file_id: int):
record = db.query("SELECT path FROM files WHERE id = ?", file_id)
if not record:
raise HTTPException(status_code=404)
return FileResponse(record["path"])This completely removes the attack surface. The user never supplies a path component — they supply an ID that maps to a path you control.
Other places path traversal shows up in AI-generated code
AI assistants generate traversal vulnerabilities beyond file downloads:
File uploads
# Vulnerable: filename from upload becomes the filesystem path
filename = request.files["file"].filename
file.save(f"/var/uploads/{filename}")
# Attacker uploads a file named ../../.ssh/authorized_keysFix: generate your own filename; never use the user-supplied one.
Zip extraction (Zip-slip)
# Vulnerable: zip entry named ../../../.bashrc
import zipfile
z = zipfile.ZipFile("upload.zip")
z.extractall("/var/extracted")Fix: check each entry’s resolved path before extracting.
Template loading
# Vulnerable: template name comes from user input
template = request.args.get("template")
return render_template(f"{template}.html")
# Attacker requests: template=../../etc/passwdFix: validate template names against an allowlist.
Where this bites vibecoders
The AI-generated file-download endpoint is the classic first exposure to path traversal: “serve files from an uploads folder” is generated with string concatenation, and the assistant’s hardening pass adds a
../filter that encoding bypasses. Testing with a few encoded payloads (..%2f,%2e%2e%2f) finds it in minutes — and it’s worth doing, because a working traversal on a dev server usually means source code and.envfiles are readable.
Checklist
- Never build filesystem paths from raw user input
- Resolve the final absolute path and verify it stays inside the allowed directory
- Prefer ID-based file lookups over filename-based ones
- Test with encoded payloads:
..%2f,%2e%2e%2f, backslashes, absolute paths - Check upload filenames, zip entries, and template names too — not just downloads
FAQ
Does a ../ blacklist fix path traversal?
No. Blacklists fail against encoding tricks: ..%2f, %2e%2e%2f, ..%5c (Windows), and Unicode variants all bypass a string-based filter. The only reliable fix is resolving the final absolute path and verifying it stays inside the allowed directory tree.
Is path traversal only a download problem?
No. It applies anywhere user input becomes a filesystem path: file uploads (a filename of ../../etc/cron.d/evil), zip extraction (zip-slip), template loading, container volume mounts, and object storage keys. The traversal pattern is the same everywhere.
Related topics
- What Is Path Traversal (Directory Traversal)?
- What Is Broken Access Control (IDOR)?
- What Is SSRF (Server-Side Request Forgery)?
- How to Review AI-Generated Code Like a Senior Engineer
- What Is the OWASP Top 10?