The naive endpoint
A junior developer writes the upload handler. They've seen tutorials. They check the extension, write the file to disk, return a URL. Done.
Five things wrong with this code. Each one is an exploit.
Five ways attackers win
1. Extension lying
The attacker uploads shell.php. The extension check fails. So they upload shell.php.jpg. The check passes (last segment after . is jpg). But Apache's AddHandler or misconfigured nginx might still execute shell.php.jpg as PHP if it sees .php anywhere in the filename. CVE-favorite for a decade.
2. Content-Type spoofing
The server checks $_FILES["avatar"]["type"]. That value is the MIME type declared by the client. The attacker sets Content-Type: image/jpeg on a PHP webshell. The server believes them. They upload shell.php with Content-Type: image/jpeg and the server happily writes it.
3. Magic-byte bypass
You upgraded the check. Now you read the first few bytes (the magic number) and confirm it's a JPEG (FF D8 FF E0) or PNG (89 50 4E 47). The attacker prepends a real JPEG header to a PHP file. Magic bytes match. File saves. Server still executes it because nothing actually parsed the JPEG content.
4. Path traversal in the filename
The user-supplied filename is ../../../../etc/cron.d/evil. The upload handler doesn't normalize the path. Now you have a cron job. Or you overwrite index.php. Or you write into .ssh/authorized_keys of a service user. Filenames must be sanitized to just a basename.
5. Stored on the same server as code
The structural mistake. Uploads live under the web root, in a directory the web server treats like any other. The attacker uploads a webshell with any of the four tricks above and just navigates to it.
The right way
The decisive line is the third one: store outside the web root. Even if all the validation above fails, a file the web server cannot execute as code can't compromise the host through this path.
Special filetypes
| Type | Why it's dangerous | How to handle |
|---|---|---|
| SVG | SVG is XML. SVG can contain JavaScript. Upload SVG → user views avatar → XSS in their session. | Re-render to PNG/JPEG server-side. Never serve user SVG directly. |
| HTML / SVG / XML | Same Origin Policy: file served from your domain can read your cookies. | Serve from a separate domain (cdn-user-content.example.com) or with Content-Disposition: attachment. |
| ZIP / TAR | Zip slip (filenames in archive contain ../ — the extractor writes outside the intended directory). | Sanitize each archive entry's name before writing. Reject entries with traversal. |
| Parsers have RCE bugs (CVE-2018-9958 PDF.js, others). PDFs can also embed JavaScript. | Render server-side to image if you can. Sandbox parsing. | |
| DOCX / XLSX | Office files are zip + XML — full XXE attack surface. Macros aside. | Treat as untrusted XML input. Sandbox parsing. Reject macros. |
| Images with EXIF | EXIF can carry malicious payloads (PHP, shell) that get processed if the image goes through an image processor with a vulnerability (ImageTragick — CVE-2016-3714). | Strip EXIF on upload. Keep ImageMagick patched. Sandbox image processing. |
Real incidents
- ImageTragick (CVE-2016-3714) — a flaw in ImageMagick let attackers achieve RCE just by uploading a crafted image. Affected millions of sites that did exactly the right thing (process images server-side) but with a vulnerable processor.
- Drupalgeddon-era webshells (2014–2018) — one of the most common post-exploitation steps after a Drupal RCE was dropping a webshell into
/sites/default/files/, which was reachable from the web. - Microsoft Exchange ProxyShell (2021, CVE-2021-34473 chain) — after exploit, attackers wrote .aspx webshells into the Exchange
FrontEnddirectory, which IIS executed. The webshells gave persistent admin access. - Equifax (2017) — not strictly file upload, but the post-exploitation involved writing JSP webshells.
The takeaway
File upload is one of the oldest attack categories on the web. The defense pattern hasn't changed much: store user files outside the web root, generate filenames yourself, re-render content to strip payloads, and verify by parsing rather than by trust. Use object storage for new systems — it makes the structural error impossible by design.
The next time you write an upload handler, the question isn't "what file types do I allow?" The question is "if this file was malicious, what's the worst it could do?" — and your design should make the answer "nothing."
References
Formatted in APA 7.
- OWASP. (2024). File upload cheat sheet. OWASP Cheat Sheet Series. https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
- MITRE. (2024). CWE-434: Unrestricted upload of file with dangerous type. Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/434.html
- Snyk. (2018). Zip slip vulnerability. https://snyk.io/research/zip-slip-vulnerability
- ImageMagick. (2016). ImageMagick security policy — CVE-2016-3714 ("ImageTragick"). https://imagetragick.com/