Intermediate · NoSQL, ORMs and data features · topic 30 of 54 · 30 min
Handling file uploads safely
Explanation
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.
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--// 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 });
});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.
| File type | First bytes (hex) | As text |
|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A | .PNG.... |
| JPEG | FF D8 FF | (not readable) |
| 25 50 44 46 2D | %PDF- | |
| GIF | 47 49 46 38 | GIF8 |
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.
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
);Presigned URLs: upload straight to storage
- The app asks your backend: "I want to upload a 2 MB PDF".
- 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.
- The app uploads the file directly to S3 or R2 with that URL. The big file never passes through your server.
- The app tells the backend "done", and the backend checks the object exists and saves the row in the database.
// 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
);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.
Watch the lesson
Handling file uploads safely · 1/7
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)
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.
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".
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
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.