File Upload Testing Checklist: Types, Size, Security and UX
A file upload field looks harmless — a “Choose file” button, that’s it. But it’s the one place where a user puts arbitrary bytes onto your server. Three classes of problems meet here: functional (wrong type, corrupt file), security (someone uploaded a web shell) and UX (a 40 MB upload with no progress and no cancel). That’s why upload is a reliable source of bugs, real incidents, and is almost always under-tested.
Below is a checklist by section, from types and size to storage. Not “click through the happy path” — actually try to break it.
Types and MIME — don’t trust the extension
The core principle: the extension and the Content-Type header are controlled by the client, so they can’t be trusted.
- Extension vs actual content. Rename
shell.phptophoto.jpg— will the server accept it? And the reverse, a valid PNG named.txt? The real type is determined by magic bytes (the signature), not by the name. - Double extension.
invoice.pdf.exe,avatar.jpg.svg— which extension does validation take, first or last? And how is the file served back? Content-Typespoofing. Via a proxy (Burp), setContent-Type: image/pngon an executable. Validating against the request MIME is a hole.- Empty or odd MIME.
application/octet-stream, missing Content-Type, exotic types. - Polyglot files. A file valid as both a GIF and JS/PHP (GIFAR and relatives). It passes the “is this an image” check — and executes as a script.
- Case and whitespace.
.JPG,.Jpg,photo.jpg(trailing space), unicode homoglyphs in the extension. - Whitelist, not blacklist. Confirm an explicit allow-list of accepted types, not “block .exe and .php”. A blacklist is always incomplete.
Size and count
- 0 bytes. Empty file — crash, 500, or a clean error?
- 1 byte / truncated. Valid header but the file is cut off — how does the processor/preview behave.
- Huge file. Over the limit, exactly at the limit, one byte over. The limit is checked on the client and on the server (the client one is trivial to bypass via the API).
- Zip bomb / decompression. If you accept archives or unpack anything (docx, huge-resolution images) — a small file that expands into gigabytes. A “pixel bomb”: a 64000×64000 PNG weighs kilobytes but asks for terabytes during resize.
- Multiple upload. Many files at once, a count limit, total batch size, one corrupt file in the middle of the batch.
- Slow upload. Slowloris-style — a very slow send of a large body: does the server hold the connection forever, is there a timeout.
Filenames
The filename is user input too, and it often ends up in a path on disk or in a response header.
- Path traversal.
../../etc/passwd,..\..\web.config— can you escape the uploads folder and overwrite someone else’s file. A classic RCE. - Special and control characters.
;,|,&,$(), newline, NULL byte%00(name truncation), quotes. - Very long name. 255+ chars, 4096 chars — filesystem and DB limits.
- Unicode and emoji. Non-Latin scripts, RTL, emoji, RTL-override
U+202E(masksexeasgpj). - Reserved names. On Windows:
CON,PRN,NUL,COM1,AUX. - Duplicates. Two files with the same name — overwrite or deduplicate? A race when the same name is uploaded simultaneously.
- Response header. If the name is echoed in
Content-Disposition— check for header injection and correct escaping.
Security — the stakes are highest here
- Where it’s stored and how it’s served. If files sit in a web-accessible folder and are served by direct link, an uploaded
.php/.jspcan execute. Correct: store outside webroot or in object storage, serve through a controller with the rightContent-TypeandContent-Disposition: attachment. - SVG is executable. SVG is XML; you can embed
<script>and event handlers. Opened inline in a victim’s browser, that’s stored XSS. Either ban SVG, sanitize it hard (e.g. DOMPurify), or serve it only as an attachment. - EXIF and privacy. A phone photo carries GPS coordinates, model, timestamp. Publishing an avatar shouldn’t publish the author’s location. Verify metadata is stripped.
- Antivirus / scan. Is the upload scanned (ClamAV/cloud scan)? Test with the EICAR file (a harmless antivirus test signature): is it caught.
- Authorization. Can user A read/overwrite user B’s file by swapping an ID or guessing a name? A direct file link needs a permission check, not URL “secrecy”.
- IDOR on download. Enumerate
?file=1001,1002— do other people’s documents come back. - XXE via office formats. docx/xlsx are zips of XML; a parser with external entities enabled = XXE.
- Cost and abuse. Upload with no rate limit = a vector to fill the disk and burn money on traffic/processing.
Processing, preview and render
- Corrupt/truncated file during preview generation — a graceful error, not a 500 and not a dead worker.
- EXIF orientation. A photo “lying on its side” — does the resizer honor
Orientation. - Resize and quality. PNG transparency, GIF/WebP animation, color profiles, oversized resolution (see the pixel bomb above).
- Async processing. If conversion runs in the background — what the user sees until it’s ready; what if the worker dies; retry.
- Idempotency. Re-uploading the same file (double tap, network retry) shouldn’t spawn duplicates or charge anything twice.
UX and reliability
- Progress. Is there an indicator for large files; is the percentage correct.
- Cancel and resume. Can you cancel; does the upload continue after a network drop (resumable).
- Network drop mid-upload. On mobile — Wi-Fi→cellular, an elevator, the subway. A partial file on the server must not count as valid.
- Drag & drop and paste from clipboard (screenshot Ctrl+V), pick from camera/gallery on mobile.
- Clear errors. “File too large (max 10 MB)” instead of “Error 413”. A forbidden-type error names the allowed types.
- Offline and retries. What happens with no network; the form must not submit twice.
- Accessibility. The button is keyboard-reachable and announced by a screen reader; upload state is announced.
Storage and lifecycle
- Where it lives: object storage (S3 and friends) vs local disk; private vs public bucket.
- Orphaned files. Uploaded but the form was never submitted — does the file live forever? Is there temp cleanup.
- Deletion. When the entity is deleted, is the file itself deleted; are the cache/CDN copies and previews purged.
- Limits and quotas per user; behavior when exhausted.
Summary checklist
- Type determined by magic bytes, not the extension or the request
Content-Type; a whitelist of allowed types - Double extensions, polyglots, MIME spoofing via proxy — defeated
- Size boundaries: 0 bytes, truncated, exactly the limit, limit+1; validation on the server, not just the client
- Zip/pixel bomb doesn’t take down processing
- Filename sanitized: path traversal, special chars, NULL, RTL-override, length, duplicates
- Files outside webroot or in storage; served by a controller as
attachment, not executed - SVG banned/sanitized; EXIF metadata stripped; virus scan (EICAR) works
- Authorization on read/delete; no IDOR on download
- Progress, cancel, network drop, drag&drop, clear errors, accessibility
- Orphaned files cleaned up; deleting the entity purges the file, previews and CDN
Tools
- Burp Suite / OWASP ZAP — spoof
Content-Type, the name, replay the request bypassing client validation. file(by magic bytes) and exiftool — check how the system determines type and what happens to metadata.- EICAR test file — verify the antivirus scan actually runs.
- Ready-made “evil” images/polyglots and decompression-bomb generators — to test processing.
Treat upload not as “attach a picture” but as accepting untrusted data onto your infrastructure. Test from that stance — and half the findings will surface on their own.