Skip to content

Intermediate · NoSQL, ORMs and data features · topic 30 of 54 · 30 min

Handling file uploads safely

Explanation

Users upload photos, PDFs and documents through multipart/form-data requests. A safe backend limits the size, checks the real file type from its first bytes, never trusts the file name, and stores the file in object storage such as S3 or Cloudflare R2. The database keeps only the file key and details.

How a file reaches the server

A normal form sends text. A form with a file is sent as multipart/form-data: the request body is split into parts, one per field, separated by a long random line called the boundary. Each part has its own small headers, such as the file name and type.

output
POST /api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----x7Fq

------x7Fq
Content-Disposition: form-data; name="title"

Marksheet
------x7Fq
Content-Disposition: form-data; name="file"; filename="marks.pdf"
Content-Type: application/pdf

%PDF-1.7 ...binary bytes...
------x7Fq--
A multipart request: a text field and a file field, separated by the boundary.
javascript
// Example only: Express + multer
import multer from 'multer';

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 5 * 1024 * 1024, files: 1 }, // 5 MB, one file
});

app.post('/api/upload', upload.single('file'), async (req, res) => {
  // req.file.buffer holds the bytes; check them before saving
  res.status(201).json({ ok: true });
});
In Node, libraries like multer or busboy (or request.formData() in newer frameworks) read multipart bodies.

Check the size and the real type

Always set a size limit, so nobody can fill your disk or memory with a 10 GB upload. Then check the type. The file name (photo.png) and the Content-Type header both come from the user, so both can lie. A virus can be renamed to cute-cat.jpg in one second.

Think of it like this: A courier label says "books", but a careful post office scans the parcel to see what is really inside. Magic bytes are the scan: the first few bytes of a file tell you what it truly is, whatever the label says.
File typeFirst bytes (hex)As text
PNG89 50 4E 47 0D 0A 1A 0A.PNG....
JPEGFF D8 FF(not readable)
PDF25 50 44 46 2D%PDF-
GIF47 49 46 38GIF8
Careful: Never use the uploaded file name as a path on disk. A name like ../../app/config.js could write outside your folder (path traversal) and overwrite your code. Generate your own key (for example a random UUID) and keep the original name only as display text, cleaned.

Where to store files

Do not keep uploads on the app server's disk. Servers get replaced on every deploy, and when you run three servers, each has different files. Use object storage instead: Amazon S3, Cloudflare R2 or Google Cloud Storage. They store any number of files cheaply and safely. Your database stores only the key and details.

sql
CREATE TABLE uploads (
  id INTEGER PRIMARY KEY,
  owner_id INTEGER NOT NULL REFERENCES users(id),
  storage_key TEXT NOT NULL,      -- e.g. uploads/2026/10/3f9c1b7e.pdf
  original_name TEXT,
  mime_type TEXT NOT NULL,
  size_bytes INTEGER NOT NULL,
  created_at TEXT NOT NULL
);
The database row describes the file; the bytes live in object storage.

Presigned URLs: upload straight to storage

  1. The app asks your backend: "I want to upload a 2 MB PDF".
  2. The backend checks the user, the size and the type, and creates a presigned URL: a link that allows one upload to one key for a few minutes.
  3. The app uploads the file directly to S3 or R2 with that URL. The big file never passes through your server.
  4. The app tells the backend "done", and the backend checks the object exists and saves the row in the database.
javascript
// Example only: AWS SDK v3 (works with S3 and Cloudflare R2)
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const key = `uploads/${crypto.randomUUID()}.pdf`;
const url = await getSignedUrl(
  s3,
  new PutObjectCommand({ Bucket: 'my-app-files', Key: key, ContentType: 'application/pdf' }),
  { expiresIn: 300 } // 5 minutes
);
The backend signs a short-lived upload URL; the client PUTs the file to it.

After the upload

  • Scan files for viruses (for example with ClamAV or your cloud provider's scanner) before other users can download them.
  • Resize images and make thumbnails in a background job (for example with the sharp library), so the upload request returns quickly.
  • Remove photo metadata (EXIF), which can contain the exact GPS location where the photo was taken.
  • Keep the bucket private and serve files through short-lived signed download links when they are not public.
Tip: Treat SVG and HTML uploads as dangerous: they can contain scripts. If you must accept them, serve them with Content-Disposition: attachment or from a separate domain.

Watch the lesson

Handling file uploads safely · 1/7

File uploads

Photos, resumes, marksheets: users upload files all the time. Let us handle them safely.

Prefer a video by a teacher? Find free videos on “file upload node js multer s3 presigned url tutorial” on YouTube ↗

In real life: Uploading documents for a government job form

A recruitment portal asks for a photo (JPEG, under 200 KB) and a marksheet (PDF, under 2 MB). The backend rejects bigger files at once, reads the first bytes to confirm the photo is really a JPEG and the marksheet really a PDF, and saves them to object storage under random keys like applications/8f2c.../photo.jpg. The database row stores only the key, size and type next to the application. A background job makes a small thumbnail of the photo for the admin screen.

Practice (0/3 done)

Task 1 · Code · Javascript

Write validateUpload(file) where file is { name, size, type }. Return an array of error messages (empty when the file is fine): "File is empty" if size is 0, "File is larger than 5 MB" if size is more than 5 * 1024 * 1024, and "File type not allowed" if type is not one of image/png, image/jpeg, application/pdf.

Task 2 · Code · Javascript

Write safeFileName(name) that turns a user's file name into a safe one: keep only the part after the last "/" or "\", make it lower case, replace every run of characters that are not a-z, 0-9, "." or "-" with a single "-", replace ".." with ".", and remove dots or dashes at the start. If nothing is left, return "file". Example: "My Photo (1).PNG" becomes "my-photo-1-.png".

Task 3 · Code · Javascript

Write detectType(bytes) that looks at the first bytes of a file (an array of numbers) and returns "png", "jpeg", "pdf" or null. PNG starts with 0x89 0x50 0x4E 0x47, JPEG with 0xFF 0xD8 0xFF, PDF with 0x25 0x50 0x44 0x46 ("%PDF").

Quick quiz

Question 1

1.Which Content-Type does an HTML form use to send a file?

Question 2

2.Why check magic bytes instead of the file extension?

Question 3

3.What should the database store for an uploaded file in S3?

Question 4

4.What is a presigned URL?

Question 5

5.A user uploads a file named "../../server.js". What is the danger?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Pagination: sending data page by page