# File uploads without surprises

> Size and type checks, storage, public versus private links, malicious files and image processing: what to settle before the first upload.

Published: 2026-05-31 · Source: https://subxcat.com/articles/safe-file-uploads · Author: SUBXCAT

In a prototype, file upload is ten lines: an `input type="file"`, a request, a save to a bucket, a URL in the database. It works the first time. Then someone uploads a two-gigabyte file, an HTML page posing as an avatar, or a passport scan that anyone who guesses the URL can open.

Every uploaded file is data from a stranger that you store and show to other people. Treat it that way.

## Size

Set a size limit at every layer: the proxy, the application and the storage rules. If files go straight to storage through a signed upload URL, the limit has to be part of that URL's conditions, or it isn't enforced at all.

Beyond a per-file limit, you need a per-user quota. A hundred files at the maximum allowed size is a problem too, especially when you're the one paying for storage.

## Type

The file extension and the `Content-Type` header both come from the client, and both can be faked. Determine the type from the content, from the first bytes of the file (its signature), and check it against a list of allowed types. Allowed, specifically. "Anything except `.exe`" doesn't work, because there are more dangerous formats than you'll remember.

A word on SVG. It isn't an image, it's XML, and it can contain JavaScript. Opened in a browser from your domain, it runs with access to your cookies. If you don't need SVG, don't accept it. If you do, sanitize it or convert it to a raster format.

## Name and location

- **You choose the file name.** A random ID, with the original name stored separately in the database. A user-supplied name in a file path means `../` and overwriting other people's files.
- **Not in the web server's folder.** Files go to object storage or outside the directory your server serves as static content. Otherwise an uploaded script might become executable.
- **From a separate domain.** User content is best served from a domain unrelated to your main app. Then even an HTML file that slips through gets no access to the session.
- **With the right headers.** For downloads, `Content-Disposition: attachment` so the browser doesn't render them as a page, and `X-Content-Type-Options: nosniff` so it doesn't guess the type on its own.

## Public and private links

Public URLs suit things everyone can see anyway: avatars, product photos. Everything else, such as documents, scans, message attachments and invoices, lives in a private bucket and is served through a signed URL with a short lifetime. The server issues that URL after checking the person is entitled to the file.

An unguessable file name isn't protection. URLs end up in logs, browser history and forwarded emails, and once they're out you can't take them back.

## Malicious files

If files uploaded by one person are downloaded by others, as with attachments, documents or shared folders, scan them for malware. Do it in the background: the file is marked as pending and hidden from others until the scan passes. Many cloud storage services can do this out of the box.

## Image processing

An image you're going to display is better re-encoded than stored as uploaded.

- **Metadata.** EXIF from a phone often includes the coordinates where the photo was taken. Re-encoding strips it, and that's about your users' privacy, not just security.
- **Pixel dimensions.** A file of a few kilobytes can decode into an image too large for your server's memory. Check dimensions before decoding and set a ceiling.
- **Isolation.** Image libraries get security fixes regularly. Keep them updated and run processing in a separate process or job with memory and time limits.

## Don't forget deletion

Account deleted, files deleted. Order cancelled, attachments shouldn't sit in storage for years. Orphaned files are both extra cost and data you're still obliged to protect even though you no longer use it.

SUBXCAT can review how uploads work in your product and put them in order without changing your storage provider.
