Magic bytes, also called a file signature, are the short sequence of bytes at the start of a file that identifies its format. Checking them tells you what a file's contents claim to be, whereas a file extension only tells you what its name claims. Neither is proof of what the file will do.
An extension is just part of the name
A file extension has no authority over what a file contains. It is a convention that helps operating systems pick a default application, and it is a few characters at the end of a string that whoever uploaded the file chose freely.
Rename a document to end in .png and nothing about it changes. The bytes are identical. Any check that reads the name and concludes something about the contents has confused a label with the thing it labels.
That does not make extension checks worthless. Checking against a list of permitted extensions is cheap, runs before you have read the file, and lets you return an error a user can understand. PortSwigger's own guidance is to check extensions against a list of permitted types rather than a list of prohibited ones, because an allowlist fails closed when something unexpected turns up while a blocklist fails open. The OWASP File Upload Cheat Sheet takes the same position. Keep the check. Just be clear that it filters intent rather than establishing fact.
Magic bytes read the file itself
Most binary formats begin with a distinctive marker so that software can recognise them without relying on a filename. A PNG opens with an eight-byte header chosen partly to survive being mangled by file transfers. A PDF opens with a percent sign, the letters PDF and a version. A ZIP archive, and therefore every modern Office document, opens with the letters PK, after the format's original author.
Reading those bytes and comparing them against a database of known signatures is how the Unix file command and the libmagic library identify formats, and it is what most languages' file type detection is doing underneath. The improvement over an extension check is real, because you are now looking at the contents rather than the label.
Two limits are worth knowing. Text-based formats often have no signature at all, so an SVG, a CSV or a JSON file gives you very little to match on. That is one reason SVG uploads need handling on their own terms, and why a CSV export can carry a formula rather than data without anything looking wrong. And a signature match tells you the file starts correctly, which is a statement about its first few bytes rather than about all of it.
There is a matching problem on the way back out. Declaring the right content type when you serve a file only helps if the browser honours it, and by default it may not: see MIME sniffing.
Where both checks stop
Here is the ceiling, and it is worth understanding before you conclude that magic bytes have solved the problem.
A polyglot file has correct magic bytes. That is the entire design. It begins as a genuine image so that content validation accepts it, while also containing a region that another parser reads as its own format. Your check is not being deceived by a forged signature; the signature is real, and the check is answering a narrower question than you were asking.
Nor do magic bytes say anything about what happens when the file is processed. A structurally perfect ZIP archive can be a zip bomb. A structurally perfect Office document can contain a macro. In both cases the format is exactly what it claims, and the risk lives in what the file asks software to do afterwards.
So the honest summary of the ladder is:
| Check | What it establishes | What defeats it |
|---|---|---|
| Content-Type header | What the client said the file is | Editing the request |
| File extension | What the filename claims | Renaming the file |
| Magic bytes | The file genuinely starts as that format | A polyglot, whose start is genuine |
| Full parse and rebuild | What survives is content you chose to keep | Nothing structural, though it changes the file |
The order worth implementing
Each rung earns its place by closing the bypasses the one below it allows, so the sensible approach is to run them in sequence and stop as soon as something fails.
- Extension against an allowlist. Cheap, fast, and produces an error message the user can act on
- Magic bytes. Confirms the contents match the claim, and rejects the straightforward mismatches
- Parse or rebuild. Re-encode images from their decoded pixels, and put documents through content disarm and reconstruction. This is the only step that reads the whole file rather than its opening bytes
Alongside those, treat the Content-Type header as a hint about intent and never as a control, and keep the structural protections in place regardless: rename files, store them outside anywhere the server would execute them, and serve them from a separate origin. Those hold even on the occasion validation is wrong, which is the property that matters most.
Our guide to file upload security covers how these fit together in a complete pipeline.
Checking the first bytes is necessary. Reading the whole file is the part that decides. The Red Eagle Tech CDR API rebuilds a document from the components that carry legitimate content, so what you store is defined by what you kept rather than by what you managed to spot. Published per-document pricing, and you can be running in minutes.