The extension lies: identify files by their magic bytes
You rename invoice.pdf.exe to invoice.pdf and the icon changes. The file does not. The extension is just a label the operating system uses to decide which program opens it: it says nothing about what is inside. An analyst does not trust it. They trust the magic bytes.
What magic bytes are
Almost every file format starts with a fixed signature in its first bytes: a marker that says "I am a PNG", "I am a PDF". It is like the file's DNA. It does not matter what you call it: if it starts with %PDF-, it is a PDF; if it starts with MZ, it is a Windows executable.
PNG 89 50 4E 47 0D 0A 1A 0A ‰PNG␍␊␚␊
JPEG FF D8 FF ÿØÿ
GIF 47 49 46 38 GIF8
PDF 25 50 44 46 2D %PDF-
ZIP 50 4B 03 04 PK··
GZIP 1F 8B
ELF 7F 45 4C 46 ·ELF (Linux binary)
PE 4D 5A MZ (Windows executable)
PHP 3C 3F 70 68 70 <?php
The extension is a label, not the content
People confuse three things with "the file type", and all three can be faked:
- The extension (
.jpg): you choose it. Windows also hides it by default, sophoto.jpg.exeshows up asphoto.jpg. That is the double extension, a phishing classic. - The
Content-Typethe browser sends on upload: the client sets it, so an attacker changes it at will. - The icon: the system draws it from the extension. Pure makeup.
The signature, on the other hand, lives inside the file. To really change it you have to rebuild the file, not just rename it.
Where this bites you
File uploads
A form that only checks the extension or the Content-Type is a sieve. The attacker uploads a webshell with an innocent name:
shell.php → renamed to shell.jpg
Content-Type: image/jpeg (fake, set by the attacker)
And to bypass validations that do check the signature, they use a polyglot: a file that is valid as an image and as code. It starts with a real GIF header and hides PHP behind it:
GIF89a;<?php system($_GET['c']); ?>
To the validator, "it starts with GIF8, it is an image". To the PHP interpreter, it is executable code.
Forensics and CTF
Unlike the attacker, here you uncover what is hidden. A kitten.jpg that will not open as an image and starts with PK in hex is really a ZIP with something inside. A PNG with data appended after the end marker. This is bread and butter in forensics and steganography challenges.
<?php.The containers that start with "PK"
A detail that trips people up: a lot of modern formats are, on the inside, ZIP files. They all start with 50 4B 03 04 (PK··):
.docx .xlsx .pptx (Office)
.apk .jar (Android / Java)
.epub (e-books)
.odt (LibreOffice)
So if a signature says "ZIP" but the name is report.docx, there is no contradiction: rename it to .zip, open it and you will see its internal structure (XML, images, classes.dex…). Half of an APK investigation starts here.
How to check it yourself
On Linux/macOS you have two pocket commands:
# See the first bytes in hex + ASCII
hexdump -C suspicious.jpg | head
# Ask the system for the real type (uses the signature, not the extension)
file suspicious.jpg
# → suspicious.jpg: PHP script, ASCII text
That file output contradicting the extension is exactly the signal you are looking for. And if you have no terminal handy, the identifier above does the same from the browser.
Defense: if your app receives files
- Validate by signature, not by extension or
Content-Type. Check the magic bytes on the server against an allowlist of permitted formats. - Rename on save. Generate the name and extension yourself from the detected type; never reuse what the user sent.
- Serve uploads outside the webroot or from a domain with no code execution, so a smuggled
.phpis never interpreted. - Mind the containers: "it is a valid ZIP" is not "it is safe". A
.docxor an.apkis a ZIP with active content.
In short
- ✅ The extension, the icon and the
Content-Typeare controlled by whoever creates the file: they prove nothing. - ✅ The magic bytes (the first bytes) are the format's real signature.
- ✅ Attack: disguise an executable or a webshell as an image (double extension, polyglots).
- ✅ Forensics/CTF: uncover what hides behind a fake extension.
- ✅ Defense: validate by signature on the server and never trust the extension.
Next time you see a .jpg that weighs 4 MB and will not open, you know where to start: look at its first bytes.