ASVS 5.0 V5.1.1 (level 2) requires documentation for every upload feature that defines the permitted file types, the expected extensions, and the maximum size, including unpacked size. It must also say how files are made safe for people to download and process, and how the application behaves when a malicious file is found.
This is a documentation requirement, but it drives code. You cannot enforce rules that nobody wrote down, and you cannot test against them either. The document is the specification your tests and your reviewers work from.
Why it matters
Upload features grow without anyone owning them. A profile picture field is added, then a document attachment, then an import for spreadsheets. Each has its own handler, its own size check, and often its own gaps. When an incident happens, the questions come fast: what types do we accept here, who scans them, where are they stored, what happens to a bad one? Teams without a written answer find out by reading code under pressure.
Writing the rules also exposes decisions that were never made. If nobody can say what the unpacked limit for a zip is, there probably is not one, and that is a zip bomb waiting to happen (see V5.2.3).
How to implement it
Keep one short document, or one section per feature in your API docs, in the same repository as the code so changes are reviewed together. For each upload feature record:
- Purpose and owner. What it is for and which team maintains it.
- Allowed types. MIME types and extensions, as a closed list.
- Size limits. Maximum upload size, maximum unpacked size, maximum file count for archives, per user quota.
- Validation. How type is verified: extension, magic bytes, parser, image re encoding.
- Storage. Where files live, who can read them, how names are generated.
- Delivery. Download headers, such as
Content-DispositionandX-Content-Type-Options, and whether a separate domain serves them. - Malware handling. What scanning happens, whether files are quarantined until clean, and what the user and the on call engineer see when a file is rejected.
A compact template as a YAML file works well because tests can read it:
upload: profile-avatar
owner: identity-team
allowed:
- { ext: png, mime: image/png }
- { ext: jpg, mime: image/jpeg }
max_bytes: 2097152
max_pixels: 16000000
storage: private bucket, random object keys
delivery: re-encoded to webp, served from static domain
on_malicious: reject with 422, log event, no retained copy
Better still, load the same file in code to set the limits, so the document cannot drift from the implementation.
Keeping the document alive
A document written once and forgotten is nearly as bad as none. Attach it to the change process. Add a line to your pull request template asking whether the change touches an upload path, and if it does, whether the upload document was updated. Make the code read limits from the same source as the document, so they cannot disagree.
import { readFileSync } from 'node:fs';
import { parse } from 'yaml';
const spec = parse(readFileSync('./docs/uploads/profile-avatar.yaml', 'utf8'));
export const avatarLimits = {
maxBytes: spec.max_bytes,
maxPixels: spec.max_pixels,
allowed: new Map(spec.allowed.map(a => ['.' + a.ext, a.mime])),
};
Add a small test that walks your route table, finds every handler that uses a multipart parser, and checks that a matching spec file exists. When someone adds an upload route without documentation, the build fails and the conversation happens before release, not after an incident.
Cover the rest of the lifecycle too. Say how long files are kept, who can delete them, and how a user requests deletion. Name who is paged when the malware scan reports a hit, and what the first steps are: quarantine, block the uploader, search for the same hash elsewhere. Reviewers and auditors reading this document should be able to answer "what happens to a bad file" without opening the source. Link the document from your security overview so new engineers find it on their first week.
How to test it
Treat the document as a checklist and verify each line.
- List every route and background job that accepts files. Compare with the documented features. Anything missing is a finding.
- For each feature, upload a file one byte over the limit, a file with a wrong extension, and a file with the right extension but different content. The behavior must match the document.
- Upload an archive that expands past the stated unpacked limit.
- Upload the EICAR test string, and confirm the documented malware path runs.
- Ask someone outside the team to read the document and state what is allowed. If they cannot, it is not clear enough.
Add a CI check that fails if a new upload route exists without a matching entry.
Common mistakes
- One vague paragraph saying "we validate uploads" with no numbers or lists.
- Documenting the intent but never checking it against the code.
- Forgetting indirect uploads, such as imports, avatars through OAuth, email attachments and support tools.
- No unpacked size limit for archives and office documents.
- A document that says nothing about what happens when a file is bad.
Quick check
What must the upload documentation say about archives?
The maximum unpacked size, ideally with a file count limit, so the limits can be enforced before extraction.
Why keep the document in the code repository?
So changes to upload behavior are reviewed together with the document, and tests or code can read the same limits.
Sources
- OWASP Application Security Verification Standard, requirement V5.1.1 (level 2)
- OWASP File Upload Cheat Sheet