Rules Hub
Coding Rules Library
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Validate upload type by sniffing content, not the Content-Type header
Determine an uploaded file's type from its actual bytes (magic number) and check against an allowlist — never trust the client-sent MIME type or extension.
Bad example
| 1 | // Next.js / formidable handler |
| 2 | const form = formidable({ |
| 3 | maxFileSize: 5 * 1024 * 1024, |
| 4 | // Only allow images? |
| 5 | filter: ({ mimetype }) => Boolean(mimetype && mimetype.includes('image')), |
| 6 | }); |
| 7 | const [, files] = await form.parse(req); |
| 8 | const file = Array.isArray(files.file) ? files.file[0] : files.file; |
| 9 |
|
| 10 | // mimetype here comes straight from the multipart Content-Type header the |
| 11 | // client chose. A shell.php sent as Content-Type: image/png passes the filter. |
| 12 | await storage.put(file.originalFilename, fs.readFileSync(file.filepath), file.mimetype); |
Explanation (EN)
The `filter` only inspects the multipart `Content-Type` header, which is fully controlled by the uploader. An attacker labels a script or HTML file as `image/png` and it passes; the server then stores and may serve a hostile payload. This is a real pattern that ships in many Next.js upload routes.
Objašnjenje (HR)
Filter provjerava samo multipart `Content-Type` zaglavlje, koje uploader u potpunosti kontrolira. Napadac oznaci skriptu ili HTML kao `image/png` i ona prolazi; posluzitelj zatim sprema i mozda posluzuje zlonamjerni sadrzaj. Ovo je stvaran obrazac koji se pojavljuje u mnogim Next.js upload rutama.
Good example
| 1 | import { fileTypeFromBuffer } from 'file-type'; |
| 2 |
|
| 3 | const ALLOWED = new Map<string, string>([ |
| 4 | ['image/jpeg', 'jpg'], |
| 5 | ['image/png', 'png'], |
| 6 | ['image/webp', 'webp'], |
| 7 | ]); |
| 8 |
|
| 9 | const buffer = await fs.promises.readFile(file.filepath); |
| 10 |
|
| 11 | // Hard cap before any further work (see size-limit rule). |
| 12 | if (buffer.byteLength > MAX_IMAGE_SIZE) { |
| 13 | return res.status(413).json({ message: 'File too large.' }); |
| 14 | } |
| 15 |
|
| 16 | // Decide the type from the bytes, not from headers/extension. |
| 17 | const sniffed = await fileTypeFromBuffer(buffer); |
| 18 | if (!sniffed || !ALLOWED.has(sniffed.mime)) { |
| 19 | return res.status(415).json({ message: 'Unsupported file type.' }); |
| 20 | } |
| 21 |
|
| 22 | const safeExt = ALLOWED.get(sniffed.mime)!; // derive extension from verified type |
| 23 | await storage.put(buildStoredName(safeExt), buffer, sniffed.mime); |
Explanation (EN)
`fileTypeFromBuffer` reads the magic bytes and returns the real MIME type, which is checked against a strict allowlist. The stored extension and Content-Type are derived from the verified type, not from client input, so a disguised payload is rejected at 415. For SVG/XML or other text-based formats that magic bytes can't fully cover, add format-specific sanitization or disallow them.
Objašnjenje (HR)
`fileTypeFromBuffer` cita magicne bajtove i vraca stvarni MIME tip, koji se provjerava prema strogom popisu dopustenih. Spremljena ekstenzija i Content-Type izvedeni su iz provjerenog tipa, a ne iz korisnickog unosa, pa se prerusen sadrzaj odbija s 415. Za SVG/XML i druge tekstualne formate koje magicni bajtovi ne pokrivaju u potpunosti, dodaj sanitizaciju specificnu za format ili ih zabrani.
Notes (EN)
Maps to OWASP File Upload Cheat Sheet and ASVS V12.1. The header/extension are hints for UX only; the byte signature is the authority.
Bilješke (HR)
Povezano s OWASP File Upload Cheat Sheet i ASVS V12.1. Zaglavlje/ekstenzija su samo savjeti za UX; potpis bajtova je mjerodavan.
Exceptions / Tradeoffs (EN)
When you delegate storage to a pre-signed URL or a managed service (S3, Cloudinary), still sniff content server-side before issuing the URL or validate via the provider's content checks — don't skip validation just because bytes don't touch your disk. Magic-byte detection does not validate text-based formats (SVG, CSV, HTML); treat those separately.
Iznimke / Tradeoffi (HR)
Kad spremanje delegiras na pre-signed URL ili upravljanu uslugu (S3, Cloudinary), i dalje provjeri sadrzaj na posluzitelju prije izdavanja URL-a ili koristi provjere pruzatelja — ne preskaci validaciju samo zato sto bajtovi ne dodiruju tvoj disk. Detekcija magicnih bajtova ne validira tekstualne formate (SVG, CSV, HTML); njih tretiraj zasebno.