Secure File Handling

By Davy Rogers

One upload feature, many ways to get owned: RCE, stored XSS, path traversal.

A file upload looks like one of the simplest features you'll ever build, and it's one of the most dangerous. You're letting a stranger put a file of their choosing onto your server, and depending on what you do next, that single feature can hand them remote code execution, store an XSS payload for every other user, let them write outside the upload directory, or simply exhaust your disk.

The root cause is almost always the same: trusting the file to be what it says it is.

The risks, concretely

  • Remote code execution. A .php or .jsp web shell that the server happily executes.
  • Stored XSS. An HTML or SVG file with embedded JavaScript, served back to other users.
  • Path traversal. A filename like ../../etc/passwd that writes somewhere it shouldn't.
  • Denial of service. Enormous files, or zip bombs that explode on extraction.
  • Malware hosting. Your server quietly distributing someone else's payload.

Never trust the metadata

The filename, the MIME type, and the extension are all set by the client, which means they're all attacker-controlled. The only thing you can trust is the actual content of the file, so validate that.

import magic
mime = magic.from_buffer(file.read(2048), mime=True)
if mime not in ["image/jpeg", "image/png"]:
    raise ValueError("Invalid file type")

Then add the other guardrails:

Size limits at the web server, the application, and the storage quota.

Re-encode images, which strips embedded scripts, EXIF data, and malformed content in one move:

from PIL import Image
img = Image.open(uploaded_file)
img.save(output, format="PNG")

Generate your own filenames rather than sanitising theirs:

safe_filename = f"{uuid.uuid4()}.png"

Where you put the file matters

Store outside the web root. A file inside the document root can be executed by the server. Drop a .php into a directory Apache serves and Apache will run it.

Prefer object storage. S3 or GCS keeps uploads off your application server entirely, so there's nothing to execute, and you get access control for free. Serve them through pre-signed URLs with a short expiry.

Serving it back safely

  • Set an explicit Content-Type from the type you validated, not from what the user claimed.
  • Force a download with Content-Disposition: attachment where it makes sense.
  • Serve uploads from a separate domain like uploads.example.com, so that an injected script lands on a different origin and can't touch your main app.
  • Add X-Content-Type-Options: nosniff so the browser doesn't second-guess your content type.

The high-risk file types

TypeRiskWhat to do
SVGEmbedded JavaScriptSanitise, or reject
HTMLScript executionServe as attachment, never text/html
ZIPBombs, path traversalCap size, sanitise paths on extraction
OfficeMacrosScan, or convert to PDF

Validate the bytes, not the label. Re-encode what you can. Store it outside the web root, and serve it from somewhere that can't hurt you.