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
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
javascript
import { useEffect, useRef, useState } from 'react'; export function useDelayedFlag(active, delay = 300) { const [visible, setVisible] = useState(false);
Delaying a loading spinner with a React hook
custom-hooks
debouncing
cleanup
Intermediate
8 steps
go
package middleware import ( "net/http"
Per-plan export limits in Gin middleware
middleware
rate-limiting
authorization
Intermediate
7 steps
javascript
const SWIPE_THRESHOLD = 80; const MAX_TRANSLATE = 120; export function attachSwipeToDismiss(element, onDismiss) {
Building a swipe-to-dismiss gesture in JS
touch-events
gesture-detection
dom-manipulation
Intermediate
10 steps
go
package httputil import ( "net"
Safely extracting the real client IP in Go
security
http
ip-spoofing
Intermediate
7 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.