Path traversal and file upload vulnerabilities
A filename is just a string until the filesystem interprets it — and the filesystem is happy to interpret "../" as "go up a directory," which means a feature that reads or saves a file by an attacker-supplied name can be tricked into touching files far outside the folder it was ever meant to access.
3 min read
Path traversal: ../ means exactly what it looks like, to the filesystem
@app.route("/download")
def download_file(request):
filename = request.args["file"] # attacker-controlled
return send_file(f"/app/uploads/{filename}")
# filename = "../../etc/passwd" turns this into:
# /app/uploads/../../etc/passwd → the filesystem RESOLVES this to /etc/passwd../ is a real, standard filesystem instruction meaning "go up one directory" — it's not something the application interprets specially; the operating system's own path-resolution logic does, exactly the same way it would for a path a developer typed intentionally. If a filename comes from user input and gets concatenated directly into a file path with no validation, an attacker can use ../ sequences to walk out of the intended uploads directory entirely and reach files anywhere else on the filesystem the application process has permission to read — configuration files, other users' data, even system files like /etc/passwd.
The fix: resolve the path, then verify it's still inside the intended directory
import os
def safe_path(base_dir, filename):
base_dir = os.path.abspath(base_dir)
full_path = os.path.abspath(os.path.join(base_dir, filename))
if not full_path.startswith(base_dir + os.sep):
raise ValueError("Path traversal attempt detected") # resolved path escapes the base directory
return full_pathThe robust fix resolves the final, actual path (following every ../ to see where it genuinely ends up, exactly the same resolution the filesystem itself would do) and then checks whether that resolved path still falls within the intended base directory — rejecting it if not. Simply checking whether the input string contains the literal substring "../" is a real, common, incomplete fix — an attacker can use URL encoding, alternate path separators, or other tricks that don't literally contain that exact substring but still resolve to the same escape, which is exactly why resolving the actual final path (not pattern-matching the raw input string) is the structurally sound approach.
File uploads: the danger isn't the upload itself — it's what happens to the file afterward
@app.route("/upload-avatar", methods=["POST"])
def upload_avatar(request):
file = request.files["avatar"]
file.save(f"/app/static/uploads/{file.filename}") # saved with the CLIENT-PROVIDED filename, AS-IS
# An attacker uploads a file named "shell.php" (or .py, .jsp — whatever
# the server actually executes) — if it lands somewhere the web server
# will EXECUTE files from, the attacker now has arbitrary code executionIf an uploaded file is saved using the filename the client provided, with no validation, and that file lands in a directory the web server treats as executable (rather than purely served as static content), an attacker can upload a file containing real, malicious server-side code with an executable extension — and then simply request that file's URL directly to have the server run it. This is a genuinely severe escalation: what looks like "just an image upload feature" can become full remote code execution if the upload path and the execution path overlap even slightly.
The real, layered defense for file uploads
import uuid
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif"}
def handle_upload(file):
ext = os.path.splitext(file.filename)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValueError("File type not allowed")
# Generate a NEW filename — never trust or reuse the client-provided one at all
safe_filename = f"{uuid.uuid4()}{ext}"
file.save(f"/app/static/uploads/{safe_filename}")A genuinely robust file-upload defense layers several things together: validate the file extension against an allowlist (not a blocklist — the same recurring "allowlist beats blocklist" principle this domain's SSRF lesson covered), ideally verify the file's actual content matches its claimed type (not just trusting the extension, which is trivially fakeable), generate a new, random filename rather than trusting the client-supplied one at all (closing the path-traversal risk in the filename itself), and — the most structurally important layer — store uploaded files in a location the web server is configured to serve as static content only, never as executable code, regardless of what extension the file happens to have.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why does `../` in a filename let an attacker escape an intended directory?
2. Why is checking whether the input string literally contains '../' an incomplete fix for path traversal?
3. Why can a seemingly harmless 'avatar upload' feature lead to full remote code execution?