How do I handle file uploads securely?

Direct answer

Never trust the filename, the extension or the content type, all three come from the client. Generate your own filename, check the file's actual bytes, cap the size, and store uploads on object storage rather than anywhere your server might execute them. Treat SVG as executable, because it is.

Muhammad HasanUpdated

Everything the browser tells you about an uploaded file is supplied by the client and can be anything.

Generate the filename

// Vulnerable — client controls the filename
const dest = path.join(UPLOADS, req.file.originalname);

Generate your own instead:

const id = crypto.randomUUID();
const ext = allowedExtension(req.file.mimetype);
const dest = path.join(UPLOADS, `${id}${ext}`);

Keep the original name in the database for display. Using it on disk gives you path traversal, filename collisions, and a set of problems with unusual characters, none of which you need.

Check the bytes, not the label

Content-Type is a header the client sets. The extension is part of a string the client chose. Neither says anything about what the file is.

import { fileTypeFromBuffer } from 'file-type';

const type = await fileTypeFromBuffer(buffer);
if (!type || !['image/jpeg', 'image/png'].includes(type.mime)) {
  throw new Error('Unsupported file type');
}

This reads the magic bytes at the start of the file. It isn't complete. A polyglot file can be valid as two formats, but it stops the straightforward version.

Use an allowlist of permitted types. Blocking .php and .exe means enumerating everything dangerous, which never finishes.

Store it somewhere inert

The serious outcome is a file that gets executed. Storing uploads inside the web root on a server that runs PHP, or anywhere a misconfiguration might interpret them, is what turns an upload into remote code execution.

Object storage. S3, GCS, R2, solves this by default. Nothing there executes. Serve through signed URLs or a proxy route that checks authorisation.

If uploads must live on the filesystem, put them outside the document root and serve them through application code that sets Content-Disposition: attachment and X-Content-Type-Options: nosniff.

SVG is code

SVG files can contain <script>. Serving a user-uploaded SVG from your domain is stored XSS.

Either exclude SVG from allowed types, or sanitise with something built for it. DOMPurify supports SVG, and serve from a separate origin so any script that survives can't reach your session cookies.

Size and count

Cap the file size at the framework level rather than checking after reading the whole thing into memory. Cap the number of files per request and per user per hour.

Archive formats need care of their own: a small zip can expand to gigabytes. If you extract uploads, enforce limits on both the total decompressed size and the number of entries.

Reprocess images

Re-encoding an uploaded image. Resizing it, even by a pixel, through a library like sharp strips embedded payloads and metadata. It costs some CPU and removes a class of problems entirely.

Keep the library updated. Image parsers have a long history of memory-safety bugs.

Authorisation on retrieval

Uploads are also a data leak if anyone can fetch anyone's file. Random filenames help, but the fix is checking ownership on the download route, the same as any other record.

Go deeper

Kolega for AppSec teams

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free