GUIDE · SECURITY
How to pass a penetration test file upload check
The file-upload findings that come up in nearly every pen test report, and how to close them properly.
Most people discover their upload form is a security problem the same way: a penetration test report lands, and there is a finding titled something like "Unrestricted File Upload" sitting in the high-severity section.
Here is what testers actually check, and how to fix each one properly rather than papering over it.
Finding 1: No malware scanning
The test. The tester uploads the EICAR test file, or a benign file with a malicious-looking name, and sees whether it is stored and served back.
Why it matters. An upload form that stores anything makes your server a distribution point. The classic scenario is not that your server gets infected — it is that your domain ends up hosting someone else's malware, and your reputation goes with it.
The fix. Scan before you store. Not after, not on a nightly cron — the moment the file arrives and before it is written anywhere permanent or made retrievable.
Finding 2: Trusting the file extension
The test. The tester renames payload.exe to invoice.pdf and uploads it. If your validation only looks at the filename, it sails through.
Why it matters. Extensions are a claim made by whoever uploaded the file, not a fact about it.
The fix. Check the file's actual content. Every format has recognisable opening bytes — %PDF for PDFs, MZ for Windows executables, PK for ZIP-based formats including Office documents. Verify that what the file is matches what it claims to be, and reject mismatches.
%PDF → PDF document
MZ → Windows executable
\x7fELF → Linux executable
PK → ZIP container (or .docx, .xlsx, .pptx, .jar)
Note the last one: an Office document is a ZIP file. To tell a harmless .docx from a macro-laden one you have to look inside for vbaProject.bin.
Finding 3: Double extensions
The test. Upload report.pdf.exe, or image.jpg.php on a PHP host.
The fix. Reject filenames with a second, executable extension outright. There is no legitimate reason for one.
Finding 4: Files served from your own domain
The test. Upload something, then request it back and inspect the response headers.
Why it matters. A stored HTML or SVG file served from your domain can run JavaScript in your users' session — stored cross-site scripting, delivered by your own upload feature.
The fix. Serve user-uploaded files from a separate domain where possible, always with Content-Disposition: attachment and X-Content-Type-Options: nosniff. Never let the browser decide the content type by sniffing.
Finding 5: No size limits or decompression-bomb protection
The test. Upload a very large file, or a small ZIP that expands to many gigabytes.
The fix. Enforce a size cap while streaming, not just by trusting the Content-Length header. If you scan inside archives, cap recursion depth and total extracted size — a good scanner does this for you, but check it does.
Finding 6: Guessable file locations
The test. Upload two files and see if the stored names are sequential or predictable.
The fix. Store with a random identifier such as a UUID, and keep the original filename as metadata only. This also removes an entire class of path-traversal bugs, because the user's filename never touches the filesystem.
Wiring scanning in
The scan needs to sit between the upload arriving and the file being stored:
curl -H "Authorization: Bearer upscan_yourkey" \
-F file=@upload.pdf \
https://upscan.desaihome.uk/v1/scans
You get a ticket immediately and the verdict follows in seconds. In practice that means: accept the upload to a temporary location, submit it, wait for the verdict, then either store it or reject it with a clear message to the user.
Decide your failure mode on purpose
The question testers ask that catches people out: what happens if the scanner is unavailable?
There are two defensible answers and one bad one:
- Fail closed — reject uploads. Safest, but your form breaks when a third party has an outage.
- Fail open — accept and log it. Keeps your site working, accepts a window of risk.
- Not having thought about it — the finding you do not want.
Whichever you choose, log every occurrence so you know when it happened.
Retesting
Before the retest, run through the tester's own checklist yourself: EICAR, EICAR in a ZIP, an executable renamed as a PDF, a double extension, an oversized file, and a macro-enabled document. If all six behave the way you intended, the finding closes.
Scan your first file free
100 scans a month, no card required. Files scanned in the UK and deleted the moment scanning finishes.
Start free