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, .jpg or .jpeg.
  • .png requires the declaration image/png; .jpg and .jpeg require image/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 READY state and current access to N17.

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.