javascript
47 lines · 7 steps
Validating file uploads by content, not just claims
Layered upload validation that checks size, extension, declared type, and the file's real magic bytes.
Explained by
highlit
1const MAX_FILE_SIZE = 5 * 1024 * 1024;
2
3const ALLOWED_TYPES = {
4 'image/jpeg': ['jpg', 'jpeg'],
5 'image/png': ['png'],
6 'image/webp': ['webp'],
7 'application/pdf': ['pdf'],
8};
9
10const MAGIC_SIGNATURES = [
11 { type: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },
12 { type: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47] },
13 { type: 'image/webp', bytes: [0x52, 0x49, 0x46, 0x46] },
14 { type: 'application/pdf', bytes: [0x25, 0x50, 0x44, 0x46] },
15];
16
17function sniffMimeType(buffer) {
18 return MAGIC_SIGNATURES.find(({ bytes }) =>
19 bytes.every((b, i) => buffer[i] === b)
20 )?.type ?? null;
21}
22
23export function validateUpload(file) {
24 const errors = [];
25
26 if (file.size === 0) {
27 errors.push('File is empty.');
28 } else if (file.size > MAX_FILE_SIZE) {
29 errors.push(`File exceeds the ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`);
30 }
31
32 const extension = file.originalname.split('.').pop()?.toLowerCase() ?? '';
33 const allowedExtensions = ALLOWED_TYPES[file.mimetype];
34
35 if (!allowedExtensions) {
36 errors.push(`Unsupported file type: ${file.mimetype}.`);
37 } else if (!allowedExtensions.includes(extension)) {
38 errors.push(`Extension ".${extension}" does not match ${file.mimetype}.`);
39 }
40
41 const detected = sniffMimeType(file.buffer);
42 if (detected && detected !== file.mimetype) {
43 errors.push(`Declared type ${file.mimetype} does not match contents (${detected}).`);
44 }
45
46 return { valid: errors.length === 0, errors };
47}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Never trust a client-declared MIME type; verify it against the file's actual byte signature.
- 2Collecting errors into an array lets you report every problem at once instead of failing on the first.
- 3Cross-checking extension, declared type, and content closes gaps any single check would leave open.
Related explainers
javascript
import { NextResponse } from 'next/server'; import { Redis } from '@upstash/redis'; const redis = Redis.fromEnv();
Sliding-window rate limiting in a Next.js route
rate-limiting
redis
sorted-set
Advanced
8 steps
ruby
class WebhookSignatureConstraint def initialize(provider) @provider = provider end
Verifying webhook signatures with Rails routing constraints
routing constraints
hmac
webhooks
Advanced
7 steps
javascript
function zip(keys, values) { if (keys.length !== values.length) { throw new RangeError('zip expects arrays of equal length'); }
Three ways to zip arrays in JavaScript
arrays
higher-order-functions
pairing
Intermediate
6 steps
javascript
import { useCallback, useEffect, useState } from 'react'; export function useLocalStorage(key, initialValue) { const readValue = useCallback(() => {
How a useLocalStorage hook syncs state in React
custom hooks
localstorage
state persistence
Intermediate
8 steps
python
from fastapi import FastAPI, Request, status from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse
Redacting sensitive fields in FastAPI errors
validation
error-handling
security
Intermediate
7 steps
javascript
import { useState } from 'react'; export function ReorderableList({ initialItems }) { const [items, setItems] = useState(initialItems);
Drag-to-reorder lists in React
drag-and-drop
state-management
immutable-updates
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/validating-file-uploads-by-content-not-just-claims-explained-javascript-49f6/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.