Secure file upload: what to check and where to store files

File upload is one of the riskiest features. A careful design turns it from a common critical bug into a safe feature.

A file passing through checks before storage

Letting users upload files (avatars, documents, images) is useful and dangerous. A weak upload can let an attacker run code on your server, store malicious content, fill your disk or attack other users.

What can go wrong

  • Remote code execution: a script file (such as .php, .jsp) saved where the server will run it.
  • Stored XSS: an HTML or SVG file served from your domain, running script in your users' browsers.
  • Path traversal: a file name like ../../config writes outside the intended folder.
  • Denial of service: huge files, many files, or "decompression bombs" that expand to gigabytes.
  • Malware shared with other users.
  • Image parser bugs in libraries that process the file.

A safe design, step by step

1. Allow-list, never deny-list. Accept only the types you need (for example JPEG, PNG, WebP, PDF). Reject everything else.

2. Check more than the extension and the Content-Type header. Both are controlled by the client. Check the file's real content (magic bytes) and re-encode images with a trusted library, which also strips hidden data.

3. Never use the user's file name. Generate a random name (for example a UUID) and a fixed extension chosen by you. Store the original name only as text metadata.

4. Store outside the web root, or in object storage. Files should not be in a folder where the server executes code. Better: upload to object storage (S3, R2) and serve through a CDN from a separate domain, so uploaded content cannot touch your site's cookies.

5. Serve safely. Send Content-Type from your own allow-list, add X-Content-Type-Options: nosniff, and use Content-Disposition: attachment for documents. Do not serve user HTML or SVG inline from your main domain.

6. Limit size and count. Set maximum file size, number of files and request rate per user. Limit image dimensions and archive expansion.

7. Scan. Run antivirus or malware scanning for shared files, ideally asynchronously in a queue before making the file available.

8. Authorise access. Check that the user may upload, and that only the owner (or permitted users) can read private files (see IDOR). Use signed, expiring URLs.

9. Least privilege. The process that handles uploads should have no rights it does not need.

Presigned uploads

Letting the browser upload directly to object storage with a short-lived presigned URL (with a size and type limit) keeps heavy and risky file handling away from your app servers.

Takeaway: allow-list types, check real content, rename files, store them away from code execution on a separate domain, limit size and scan.


Web Security series, Part 2 (more bug classes and hardening): 3 of 6. Previous: CORS explained: what it is and how it gets misconfigured.

Keep reading