Requests, files, and servers
Handle file uploads
Validate upload purpose, size, type, content, storage, permissions, and delivery without trusting filenames or browser-provided metadata.
An upload is untrusted bytes plus untrusted metadata. The filename, the extension, and the Content-Type all come from the client and can all lie. Treat both parts as hostile until the server checks them.
A file named avatar.png carries HTML and script. If the server trusts the name and serves it inline from the application origin, the upload becomes active content.
Limit, inspect, rename, isolate
Set a hard size limit at the parser so a huge upload cannot exhaust memory or disk.
const upload = multer({ dest: '/srv/uploads', limits: { fileSize: 2 * 1024 * 1024 } })
Verify the content, not the claimed type. Sniff the real bytes and reject anything that is not on your narrow allowlist.
import { fileTypeFromFile } from 'file-type'
const kind = await fileTypeFromFile(req.file.path)
if (!kind || !['image/png', 'image/jpeg'].includes(kind.mime)) {
return res.status(415).end() // not a real image: reject
}
Generate the storage name yourself and keep files outside any path the server will execute. Then serve them through a dedicated route that sets a safe type and forces download rather than inline rendering.
res.set('Content-Type', 'image/png')
res.set('Content-Disposition', 'attachment; filename="download.png"')
res.set('X-Content-Type-Options', 'nosniff')
Deep scanning adds time and can still miss new formats. Start with a narrow accepted format, strict size, verified parsing, generated storage names, and safe download headers.
Upload a valid image, an oversized image, HTML renamed as PNG, a malformed image, and two files with the same name. Record the decision, stored name, detected type, and response headers for every case.
Lesson completed