Software development

Build a file upload workflow with a quarantine boundary

Start with private intake, then bind review and retrieval to the same object generation. The local fixture makes a delayed approval safe to reject.

Executed checks
Six sequential release-state assertions with synthetic worker verdicts
Core guard
Approval and retrieval refer to the exact current object generation
Integration boundary
No malware scanner, cloud policy or real authentication service tested
Files wait inside an amber quarantine enclosure while a separate teal tray holds a released document.
Conceptual illustration of private intake separated from approved file delivery.

Make uploaded different from available

Give an uploaded file a private intake state before any customer can download it. A successful transfer proves that bytes reached storage. It does not prove that the application has checked their type, approved their contents or associated them with the right tenant.

This walkthrough builds a small release-state model. Its central rule is that approval belongs to one exact object generation. Replace those bytes and the old approval stops applying. You will run six local checks, including a delayed positive result for an obsolete generation. The verdicts are synthetic test inputs: no malware scanner, cloud bucket or authentication service is exercised.

Use the Python fixture and recorded results. Save the fixture as experiment.py, then run python3 experiment.py. Read it alongside the integration worksheet, which identifies what remains to be implemented outside the model.

For production intake, choose a server-generated upload identity and resolve its tenant from authenticated context. Place incoming bytes in storage that the customer-facing download path cannot read directly. Keep the original filename as display metadata only if needed. It should not determine a storage path or authorize a download.

Bind the work to an immutable generation

Record the exact object generation, not just its key. In the local fixture, receiving bytes increments a generation counter and computes their SHA-256 digest. A processing request carries both values. A new receive resets state to scanning, even when the previous generation had been approved.

That generation counter is an in-memory teaching device. In an actual service, use an immutable object reference or a storage version identity whose guarantees you have verified. The scanner must read that exact reference. Its result must identify the same reference, and the retrieval path must serve it. Hashing one read and later fetching an unversioned key leaves a replacement window between those operations.

The distinction matters for direct uploads. AWS documents that an S3 presigned URL can be used repeatedly until expiration and that uploading to an existing key replaces the object. A URL is therefore not automatically a one-use completion token. Scope upload permission, prevent an uploader from changing released storage, and do not treat a browser's completion callback as authority over the object's identity.

Worker reads generation1, then replacement creates generation2. Positive result for1 cannot approve2. Current result and retrieval must refer to the same immutable generation.
Figure 1. Generation binding prevents an older processing result from releasing replacement bytes. View full-size figure.

Before enqueueing inspection, verify the stored object's identity and applicable intake limits on the server. An abandoned multipart transfer, an oversized object and a caller-provided digest need their own validation paths. A digest computed by trusted processing identifies bytes. By itself it says nothing about whether those bytes are acceptable.

Accept a verdict only for current work

The model's verdict function first checks state and identity. If the current generation differs from the scanned generation, it returns false without changing the record. If identity matches, a positive synthetic verdict sets approved. A negative one sets rejected.

Illustrative procedure
if self.state != 'scanning':
    return False
if (generation, digest) != (self.generation, self.digest):
    return False
self.state = 'approved' if passed else 'rejected'

Run the delayed-result case: receive A, start work for generation 1, replace it with B as generation 2, then deliver a positive result for generation 1. The expected state remains scanning for generation 2. A generic upload_id → approved update would lose the distinction and could release B based on work performed on A.

In a database, perform the identity comparison and state transition atomically. Include the applicable processing attempt or policy revision if retries and policy changes can produce competing verdicts. A worker timeout must remain pending, failed or otherwise unavailable according to your documented policy. It must not become a positive result because an error handler needs to finish a job.

The function assumes a trusted worker caller. Production code must authenticate that worker and reject client-submitted approval. It also needs a defined policy for reinspection, cancellation and revocation. The fixture demonstrates the generation guard, not those surrounding controls.

Put the same guard on retrieval

Check the requesting tenant and approved state before serving bytes. In the fixture, download returns a body only for the owning tenant while state is approved. Receiving another generation immediately removes that availability.

If your application issues a download URL, perform authorization before issuing it and bind the URL to the approved immutable object. Specify how long previously issued access remains usable after revocation. A database flag change does not necessarily invalidate an already usable storage capability or a cached response.

Also keep quarantine inaccessible through alternate paths: preview generation, document conversion, email attachment delivery and background extraction are consumers of untrusted bytes too. Do not give those processors wider access just because the interactive download endpoint has a guard. Their resource limits and isolation matter before they begin parsing.

OWASP's file upload guidance calls for layered controls, including allowed types, size limits, restricted storage and appropriate content inspection. It specifically cautions against trusting the client-supplied content type. The release-state guard supplements those checks. It cannot determine file safety on its own.

Verify the failures before integrating storage

Read each case's persisted state in the results file. A rejected stale result should leave the new generation awaiting review, rather than quietly approving it or making it unavailable forever.

Six executed state-machine checks and their expected observations
Executed checkExpected observation
Before a verdictOwning tenant receives no bytes
Positive result for obsolete generation 1Rejected; generation 2 remains scanning
Negative result for current generation 2Rejected state; download remains blocked
Positive result for current generation 3Exact C bytes become available
Different tenant requests generation 3No bytes returned
Replacement creates generation 4Prior approval resets; old result cannot restore it
Six checks passed: before-verdict blocked, stale result rejected, current negative blocked, current positive serves exact bytes, other tenant blocked, replacement resets approval.
Figure 2. Executed local assertions. Worker verdicts are synthetic inputs and do not establish malware detection effectiveness. View full-size figure.

All six assertions passed in the sequential model. They establish control-flow outcomes for assigned verdicts, not antivirus effectiveness, concurrent transaction safety or real storage permissions. Keep those evidence categories separate when turning this example into an endpoint.

For integration acceptance, repeat the replacement sequence through the real object store, queue and download handler. Confirm the worker read the approved version and that the response served the same version. Attempt direct access to quarantine, a forged result, another tenant's identifier, duplicate completion and a stalled processor. Record which boundary rejects each attempt. These checks turn a green status label into a reviewable release decision.

Sources

Documentation checked .

  1. OWASP: file upload protection
  2. AWS: presigned URL reuse and object replacement