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.
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.
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.
| Executed check | Expected observation |
|---|---|
| Before a verdict | Owning tenant receives no bytes |
| Positive result for obsolete generation 1 | Rejected; generation 2 remains scanning |
| Negative result for current generation 2 | Rejected state; download remains blocked |
| Positive result for current generation 3 | Exact C bytes become available |
| Different tenant requests generation 3 | No bytes returned |
| Replacement creates generation 4 | Prior approval resets; old result cannot restore it |
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 .
