Skip to content
Back to insights
CybersecurityMay 18, 202612 min read

Secure File Uploads in FastAPI: Path Traversal Defense and Bandit SAST Setup

A practical backend security guide for FastAPI file uploads, including path traversal prevention, file-size limits, extension allowlists, and Bandit static analysis.

Cresnex logo

Cresnex Editorial

Research-led analysis built for readability, trust, and future monetization.

Reviewed under the Cresnex editorial policy. Last reviewed: May 18, 2026.

Backend security

Editorial context for readers who want the signal without the noise

Cresnex research note

Key takeaways

  • Never write user-supplied filenames directly to storage paths.
  • Enforce file-size limits and allowlists before saving uploads.
  • Bandit helps catch insecure Python patterns before they reach production.

Why file uploads are a security boundary

A file upload endpoint accepts untrusted data, writes to infrastructure, and often triggers downstream parsing. That makes it one of the easiest places for small mistakes to become serious vulnerabilities.

Common risks include path traversal, oversized payloads, executable files, malicious archives, parser exploits, and accidental exposure of stored objects.

The safest pattern is to treat the original filename as display metadata only. Storage paths should be generated by the server, not chosen by the user.

Related context

Continue with the wider Cresnex research library

This article is part of a broader Cresnex library on cybersecurity, AI risk, online fraud, and India-specific digital trust. Use the links below to continue reading related explainers and research briefs.

A safer FastAPI upload route

The route below combines extension allowlisting, explicit size checks, random server-generated filenames, and controlled write paths. It is intentionally simple so the security decisions remain visible.

For production systems, pair this with authentication, malware scanning for risky file types, object storage permissions, and background processing instead of trusting the upload immediately.

app/main.pypython
from pathlib import Path
from uuid import uuid4

from fastapi import FastAPI, File, HTTPException, UploadFile, status

app = FastAPI()

UPLOAD_DIR = Path("/tmp/cresnex-safe-uploads").resolve()
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)

MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024
ALLOWED_EXTENSIONS = {".txt", ".json", ".csv", ".png", ".jpg", ".jpeg"}


@app.post("/api/v1/secure-upload")
async def secure_upload(file: UploadFile = File(...)):
    original_name = file.filename or ""
    file_ext = Path(original_name).suffix.lower()

    if file_ext not in ALLOWED_EXTENSIONS:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="File extension is not allowed.",
        )

    content = await file.read()

    if len(content) > MAX_FILE_SIZE_BYTES:
        raise HTTPException(
            status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
            detail="File exceeds the 5 MB limit.",
        )

    safe_name = f"{uuid4().hex}{file_ext}"
    destination = (UPLOAD_DIR / safe_name).resolve()

    if UPLOAD_DIR not in destination.parents:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid upload path.",
        )

    destination.write_bytes(content)

    return {
        "status": "stored",
        "saved_as": safe_name,
        "bytes": len(content),
    }

Directory escape defense: do not trust filename cleanup alone

Functions that strip slashes or normalize filenames can help, but they should not be your only defense. Attackers look for double encoding, unicode edge cases, platform-specific behavior, and archive extraction quirks.

Generating a new server-side filename is stronger because the uploaded name never controls the path. Resolving the destination and checking that it remains inside the intended directory adds another guardrail.

If you later extract archives, apply the same rule to every member path before writing anything to disk.

Add Bandit SAST to catch insecure Python patterns early

Bandit is a static analysis tool for Python security. It scans source code for risky patterns such as hardcoded secrets, unsafe deserialization, shell injection, weak cryptography, and insecure temporary-file behavior.

A security linter will not replace code review, but it raises the floor. It catches issues that busy teams often miss when they are focused on feature delivery.

terminalbash
python -m pip install bandit
bandit -r ./backend -ll

# Optional JSON output for CI artifacts:
bandit -r ./backend -f json -o bandit-report.json
.pre-commit-config.yamlyaml
repos:
  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.10
    hooks:
      - id: bandit
        args: ["-r", "backend", "-ll"]

Production upload checklist

Require authentication when uploads are user-specific. Store files outside the web root or in private object storage. Use short-lived signed URLs instead of public paths for sensitive files.

Log metadata such as user ID, file size, content type, generated name, and scan status. Avoid logging raw secrets, file contents, or personal documents.

For images and documents, consider asynchronous malware scanning, content-type sniffing, and safe transformation before making files available to other users.

Security review question: what happens if an attacker uploads ten thousand files, one massive file, or a file that looks harmless but is parsed by another service?

FAQ

Reader questions

What is path traversal in file uploads?

It is an attack where a filename or archive path tries to escape the intended upload directory, for example by using ../ segments or platform-specific path tricks.

Does Bandit secure a FastAPI app automatically?

No. Bandit is a static analysis tool that finds risky code patterns. It should be paired with validation, reviews, tests, dependency scanning, and runtime controls.

Newsletter

Stay ahead of digital risk

Get curated research, cyber alerts, AI trend breakdowns, and strategic insights delivered from Cresnex.

Early subscription requests route through email. No spam, ever.

Related posts

Continue reading within the Cresnex archive