On this page
  1. The AI-generated pattern (vulnerable)
  2. Why naive fixes don’t work
  3. The correct fix
  4. The even simpler fix: don’t take filenames at all
  5. Other places path traversal shows up in AI-generated code
    1. File uploads
    2. Zip extraction (Zip-slip)
    3. Template loading
  6. Checklist
  7. FAQ
    1. Does a ../ blacklist fix path traversal?
    2. Is path traversal only a download problem?
  8. Related topics
  9. Sources
how-to

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/passwd walks 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 backslashes

A 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:

  1. Resolve the full path first — this normalizes all ../ sequences
  2. Verify the result starts with the base directory
  3. 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_keys

Fix: 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/passwd

Fix: 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 .env files 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.


Sources

Share: