All posts

How to Handle File Uploads Safely

File uploads are a classic attack door. Validation, random names, safe storage, and presigned URLs, with working code.

MU

Muhammad Usman

April 25, 2026 · 2 min read

Letting users upload files is normal: avatars, documents, receipts. It is also one of the oldest ways to attack an app, because an upload is literally a stranger putting a file on your server. The rules below close the door.

Validate the real type and size

The file extension and the client-sent MIME type are both user-controlled lies. Check the actual bytes and enforce a hard size limit:

import { fileTypeFromBuffer } from 'file-type'

const MAX_SIZE = 5 * 1024 * 1024   // 5 MB

if (file.size > MAX_SIZE) throw new Error('File too large')

const type = await fileTypeFromBuffer(file.buffer)
const allowed = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']
if (!type || !allowed.includes(type.mime)) throw new Error('File type not allowed')

Random names, safe location

import { randomUUID } from 'crypto'
import path from 'path'

// invoice.php becomes 3f2a...b81.pdf, and can never execute
const ext = type.ext                      // from the byte check, not the filename
const safeName = randomUUID() + '.' + ext
const dest = path.join(UPLOADS_DIR, safeName)   // outside the web root

Better: presigned uploads, skip your server entirely

The modern pattern: your server hands the browser a one-time upload URL for object storage (S3, R2, Supabase Storage). The file goes straight to storage and never touches your app server:

// Server: issue a one-time upload URL
const command = new PutObjectCommand({
  Bucket: 'user-uploads',
  Key: `avatars/${randomUUID()}.webp`,
  ContentType: 'image/webp',
  ContentLength: file.size,
})
const url = await getSignedUrl(s3, command, { expiresIn: 60 })

// Browser: upload directly to storage
await fetch(url, { method: 'PUT', body: file })

Serve user files from a separate domain

Files served from usercontent.yourapp.com (or the storage provider’s domain) can never run as your site, which kills a whole class of XSS attacks. As a bonus, re-encode images with a library like sharp: it strips malicious payloads and gives you optimized sizes for free.

Frequently asked questions

Why is renaming uploaded files important?+

A file named invoice.php uploaded to the wrong place can execute as code on the server. Random names plus storage outside the web root make that path impossible.

Should images be processed after upload?+

Yes. Re-encoding with a library like sharp strips malicious payloads and metadata, and produces optimized sizes as a bonus.

What is a presigned URL?+

A short-lived URL your server signs that lets the browser upload one specific file directly to object storage, without the file passing through your app server.

More posts