Turn File Upload Rules into Observable Outcomes
Drafted and reviewed using AI assistance.
On this page
A progress bar reaches 100%, but the new attachment still needs checking. Should the interface say “Uploaded,” “Ready,” or “Could not check”? Answer that before asking an AI coding agent to build an upload form.
A useful upload brief defines which files are eligible, when they become available, and what happens when the application cannot reach a decision. Start with a few fictional file descriptions and follow each one through the same rules.
OWASP treats extensions, declared content types, content inspection, size limits, storage and access permissions as separate concerns. A client-supplied content type can be false, and a file signature alone does not establish safety. Its guidance supports layered checks; it does not supply the specific product policy below. OWASP File Upload Cheat Sheet
Choose one attachment job
Imagine a fictional notebook accepting PNG and JPEG reference images for record N17. Mira may attach and retrieve images on that record. Sol has no access to it. These are synthetic permission labels; no accounts or files are used in this example.
For this notebook, choose the following rules:
- A file must contain between 1 and 2,097,152 bytes, inclusive. The limit applies to file bytes, excluding request-envelope overhead. The interface displays “Maximum 2 MiB per image.”
- The basename before the final extension has 1–60 ASCII letters, digits, spaces, underscores or hyphens, starts with a letter or digit, and ends with a letter or digit. A one-character basename satisfies both conditions. The final extension is exactly lowercase
.png,.jpgor.jpeg. .pngrequires the declarationimage/png;.jpgand.jpegrequireimage/jpeg. A mismatch is rejected rather than silently corrected.- The service's content classification must agree with the declared type. A separate configured content check must also return a pass before release. An unavailable checker or an unknown result never becomes a pass.
- Uploaded names are display metadata. The application allocates a separate storage identifier; it does not use the supplied name as a storage path.
- Receipt and checking take place in private staging. Retrieval requires both the
READYstate and current access toN17.
These strict filename and agreement rules are choices for this example. A different application might normalize extensions or accept other formats, but its brief should say so. This policy does not include archives, document conversion or executable attachments.
Assign each decision to a boundary
| Question | Interface responsibility | Service responsibility | If the service cannot establish it |
|---|---|---|---|
May this identity attach to N17? |
Hide unavailable controls; explain a known refusal | Check current permission before creating an attachment | Refuse; create no attachment on the record |
| Is the file within the byte limit? | Show the limit and catch obvious excess | Enforce the limit while receiving file bytes | Stop accepting the attachment; do not release it |
| Does the supplied name fit the policy? | Show a useful naming error | Validate it and allocate a separate storage identifier | Reject the attachment |
| Does extension agree with declared type? | Offer an early correction | Apply the declared mapping again | Reject the attachment |
| Does content classification agree? | Display the returned state | Classify content independently of the declaration | Keep unavailable with a checking error |
| Does the configured content check pass? | Show checking separately from transfer progress | Require a completed pass | Keep unavailable; show failure or uncertainty accurately |
| May this identity retrieve the image now? | Show an available attachment only when authorized | Check current record access and release state on retrieval | Refuse retrieval; return no file content |
The interface provides fast feedback. The service makes the acceptance and access decisions. If the request arrives despite a hidden button or a bypassed local check, those decisions still apply.
Work through nine fictional descriptors
The content categories and check outcomes below are stipulated inputs to a paper exercise. They are not results from inspecting images, running a scanner or uploading bytes. For all rows except F5, the uploader is Mira. Every filename fits the naming rule.
| ID | Supplied name | Bytes | Declared type | Stipulated content category | Separate content-check result | Expected outcome |
|---|---|---|---|---|---|---|
| F1 | sketch.png |
2,097,152 | image/png |
PNG | Pass | Eligible at the exact limit; private receipt, checking, then READY |
| F2 | sketch-large.png |
2,097,153 | image/png |
PNG | Not run | REJECTED: FILE_TOO_LARGE |
| F3 | banner.png |
2,048 | image/jpeg |
Not examined | Not run | REJECTED: NAME_TYPE_MISMATCH |
| F4 | preview.jpg |
4,096 | image/jpeg |
PNG | Not run | REJECTED: CONTENT_TYPE_MISMATCH |
| F5 | private.png |
1,024 | image/png |
Not examined | Not run | Sol is the uploader: REJECTED, NOT_ALLOWED; no attachment created |
| F6 | draft.png |
1,024 | image/png |
Unknown | Not run | CHECK_FAILED: TYPE_UNCONFIRMED; unavailable |
| F7 | diagram.png |
1,400 | image/png |
PNG | Fail | REJECTED: CONTENT_CHECK_FAILED; unavailable |
| F8 | chart.png |
1,500 | image/png |
PNG | Timeout | CHECK_FAILED: CHECK_UNAVAILABLE; unavailable |
| F9 | layout.jpeg |
1,750 | image/jpeg |
JPEG | Pending | CHECKING; unavailable until a conclusive result |
F1 answers the boundary question precisely: “up to” includes the limit in this policy. F2 exceeds it by one byte. F3 has inconsistent metadata before content inspection begins. F4 has consistent metadata but contradictory content classification. Those are different failure messages and different checks.
An empty file is also rejected with EMPTY_FILE. A name such as diagram.gif is rejected with NAME_NOT_ALLOWED, even if its content is declared as PNG. A zero-byte file or invalid name is not “checking” while the application hopes a later step will repair it.
For this example, evaluate permission first, then name, byte range, extension/declaration agreement, content category and the separate content check. During a real transfer, the size gate also has to stop excess data as it arrives. If several conditions are wrong, the first applicable decision supplies the visible error. No later stage can turn that refusal into an accepted attachment.
Give each state a user-facing meaning
| State | Meaning in this example | What the user sees | File retrieval |
|---|---|---|---|
LOCAL_SELECTED |
Selection exists on the client | Selected; not sent | No remote attachment |
RECEIVED_PRIVATE |
Eligible bytes are staged privately | Received; waiting for checks | Unavailable |
CHECKING |
A required check is still pending | Checking image | Unavailable |
READY |
Required checks passed and release was recorded | Ready to attach | Allowed only with current record access |
REJECTED |
A policy condition failed | Specific refusal and a correction when possible | Unavailable |
CHECK_FAILED |
The service could not establish a required result | Could not finish checking; image unavailable | Unavailable |
EXPIRED |
An unavailable staging record reached its expiry deadline | Staged image expired; select it again | Unavailable, including while byte removal is pending |
Only the service can confirm READY. Transfer progress reaching 100% does not make that transition. Nor does an attachment receipt containing an internal identifier.
Suppose F9 receives a completed pass later. It can move from CHECKING to READY. If it receives a failure, it moves to REJECTED. If the checker times out, it moves to CHECK_FAILED. A retry of checking is an explicit user action for the same staged file, allowed only while the staging record still exists and the user still has permission. It returns to CHECKING, not directly to READY.
This notebook chooses to expire unavailable staged objects 24 hours after receipt. Rechecking does not extend that deadline. At expiry, mark the staging record expired and request byte removal; report deletion as complete only when the storage operation is confirmed. The storage mechanism and cleanup implementation still need to be selected. An expiry policy is not evidence that bytes were actually removed.
After F1 becomes READY, Mira may retrieve it. Sol still may not. Sharing its identifier does not change that decision. If Mira's record access is removed, her next retrieval is refused too. A real design must make this rule apply to the actual delivery path, including any generated download links, rather than promise it only in the attachment list.
Copy a brief with visible outcomes
Design an image-attachment flow for fictional record N17.
Use only the synthetic descriptors F1-F9 in this article. Do not create
image files, inspect private files, call scanning services or upload data.
Apply the stated PNG/JPEG, filename and inclusive byte rules exactly.
Mira may attach and retrieve; Sol has no record access. The service
must decide permission and enforce the gates even if local checks
were skipped. Never use the supplied name as the storage path.
Keep private receipt, checking, readiness, rejection and inconclusive
checking separate. Only completed required passes permit READY.
Use the exact outcome and error code for every descriptor, plus the
empty-file and unsupported-name cases. Transfer progress is not release.
Show the staged-file retry, 24-hour expiry and confirmed-cleanup states.
Require current record permission on the real retrieval path, including
links; an identifier or old UI state must not grant access.
Return the responsibility matrix, state transitions, user copy and
expected results. List the unchosen storage and content-check mechanisms
for implementation review. Do not describe the design as a secure
upload implementation or a completed integration.
The brief is ready for a design discussion when each descriptor leads to one understandable state and a clear next action. Implementation can then address how the service enforces those decisions without asking users to guess what “upload succeeded” means.