Security

Accepting files from strangers

An upload endpoint accepts arbitrary bytes chosen by someone you don't know. Almost every mistake here comes from trusting what they say it is.

18 March 2026 2 min read Mohammad Aaquib Jawed

File upload is one of the few features where you invite an untrusted party to place arbitrary bytes on your infrastructure. Most of the vulnerabilities come from believing what they tell you about those bytes.

Never trust the filename

The filename is user input. It can contain path traversal sequences, null bytes, or a thousand characters of unicode.

Never use it to construct a storage path. Generate your own identifier and store the original name as metadata for display only — escaped when rendered, because a filename is a perfectly good XSS vector.

Never trust the extension or the content type

Both are supplied by the client. A file named .jpg with an image content type can contain anything at all.

Detect the type from the content itself — magic bytes — and validate against an allowlist of types you actually support. An allowlist, not a blocklist: blocklists are an endless game against extensions you did not think of.

For images specifically, re-encoding is the strongest move available. Decode and re-encode through an image library, and anything embedded that was not image data does not survive the round trip.

Serve them from somewhere they cannot execute

A file uploaded into a directory your web server will execute is the classic path to remote code execution.

Store uploads outside the document root, or in object storage. Serve them through a controller that sets the content type explicitly, or via signed URLs from storage.

Two headers matter when serving. X-Content-Type-Options: nosniff stops browsers second-guessing your declared type. And Content-Disposition: attachment for anything that is not a type you deliberately render inline — an uploaded HTML file served inline from your origin is stored XSS with your cookies attached.

Serving user content from a separate domain is the stronger version of this, because it removes same-origin access entirely.

Uploads should be served from an origin where a malicious file cannot reach anything worth having.

Limits, and where they must be enforced

Size limits belong at the web server or proxy, before the request reaches your application. An application-level check happens after the bytes have already arrived.

Beyond size, be wary of formats that expand: archives that decompress to enormous sizes, images with dimensions that consume gigabytes when decoded. Validate declared dimensions before decoding, and cap decoded size.

Direct-to-storage does not remove the problem

Signed uploads straight to object storage are efficient and skip your servers. They also skip your validation.

The pattern that works is to validate after the fact — a job that inspects the object, verifies the type, re-encodes if appropriate, and only then marks it usable. Until that completes, nothing should serve it.

All writing Reply by email