AWS Builder Center

Presigned S3 Uploads: Integrity Is Not Authorization

A conceptual single-part upload design that separates permission to upload, checksum verification, and permission to consume, with explicit limits and a validation checklist.

Conceptual demonstration. Documentation reviewed October 2, 2026. No AWS infrastructure was created, uploads were performed, or test results measured for this article.
A successful upload is not the same as a safe document. A private S3 bucket, a short-lived presigned URL, and a checksum solve different problems. Treating them as one security feature can leave an application accepting the right bytes from the wrong user, or releasing a file it has never inspected.

Three questions, three boundaries

Authorization asks: is this signed-in application user allowed to create this upload? Integrity asks: did S3 receive the expected bytes? Release asks: may another component read and act on this object? The design below keeps those decisions separate. The workflow is an application design recommendation, not an AWS-managed approval system.
Conceptual flow: authenticate user -> authorize upload -> create backend upload record -> issue bounded PUT permission -> upload to private staging -> verify object -> inspect content -> release to a consumer.

1. Establish a backend-owned upload record

For the demonstration, assume a small sample file whose reference bytes are known to the backend, a private general-purpose S3 bucket using SSE-S3, and one single-part PutObject upload. Choose the bucket region explicitly and generate a unique staging key on the backend. Do not accept an arbitrary destination key from the caller.
Store the authorized owner, bucket, key, expected byte count, expected SHA-256, intended content type, and expiry in a backend-controlled record. An application upload ID should identify that record. A completion request should reference the ID, not provide a new checksum or destination that silently replaces the original expectations.
If the checksum originates from an untrusted client rather than trusted reference bytes, it still helps establish byte consistency. It does not prove that the file is harmless, authentic, or permitted. Content-Type is also metadata, not content inspection.

2. Define a narrow signing contract

AWS documents presigned URLs as bearer tokens with the signing principal's permissions. They can be reused until expiry, and an ordinary PUT to an existing key replaces its object. Use a signing role with only the necessary object permissions, protect the URL as a credential, and avoid placing it in application logs. [1]
For this conceptual contract, configure Boto3 with Config(signature_version="s3v4"). The current Boto3 documentation warns that backward-compatible SigV2 behavior can leave parameters unsigned or drop conditional headers. Explicit SigV4 matters when those parameters are part of the design. [2]
Presign the put_object method with Bucket and Key from the upload record, ContentType="application/octet-stream", ChecksumSHA256 set to the Base64-encoded SHA-256 digest, and IfNoneMatch="*". Use ExpiresIn=300 and HttpMethod="PUT". These are illustrative choices, not universal production defaults. Boto3 accepts the underlying method arguments through Params. [2, 3]
IfNoneMatch="*" makes the write conditional on the key not already existing: an existing object produces 412 Precondition Failed, and a conflicting operation can produce 409 ConditionalRequestConflict. This is create-only behavior, not a general one-use token. Deletion or another write path still needs its own controls. [3]

3. Upload exactly the signed request

The client sends the raw sample bytes using PUT, with the exact signed Content-Type, x-amz-checksum-sha256, and If-None-Match header values. The SHA-256 checksum is Base64-encoded, not a hexadecimal digest. S3 independently calculates the checksum and rejects an upload when the supplied value does not match. A single PutObject uses a full-object checksum. [1, 3, 4]
Do not extend this full-file SHA-256 comparison unchanged to multipart uploads: the current S3 documentation distinguishes full-object and composite checksums, and lists multipart SHA-256 as composite. This demonstration deliberately stays single-part. [4]
For a cross-origin browser client, configure CORS for the actual application origin, PUT method, and request headers. CORS enables the browser exchange; it does not replace bucket policies or authorization. Keep the request method, headers, and URL query intact to avoid signature mismatches. [1, 6]

4. Verify before releasing the object

After the client reports completion, the backend retrieves the upload record and calls head_object for its bucket and key with ChecksumMode="ENABLED". Compare the returned ChecksumSHA256, ContentLength, and ContentType to the stored expectations. The verifier needs s3:GetObject permission. SSE-KMS checksum retrieval has additional KMS permission requirements; the SSE-S3 assumption keeps this example narrower. [5]
A successful metadata comparison is only the integrity gate. Apply the application's content validation, malware inspection where appropriate, and authorization checks before permitting consumers to use the object. Those are deliberate application responsibilities, not outcomes guaranteed by a checksum.
Also protect the interval between verification and consumption. A random key and a short expiry do not establish immutability. Enforce that the verified staging object cannot be replaced, or use a versioned design that pins the verified VersionId when the consumer reads it. Otherwise, a later read might not refer to the bytes that passed verification.

Expiry is an upper bound, not a complete lifecycle

The 300-second URL cannot outlive its underlying credentials. Temporary role-session expiry or credential revocation can invalidate it earlier. AWS checks expiry when the request begins; do not describe it as an automatic mid-transfer cutoff. A bucket policy can additionally constrain signature age using s3:signatureAge, whose unit is milliseconds. An older signature can therefore be denied even if the URL's nominal expiry has not passed. [1, 7]

Validation plan, not test results

Before implementing this design, validate the intended behavior for: correct bytes; a changed byte with the original checksum; a mismatched signed header; an already-existing staging key; expired credentials; a denied CORS preflight; and a consumer attempting access before release. Add a race case in which replacement is attempted after verification. None of these cases was executed for this article.
The useful boundary is simple: permission to upload, evidence of byte integrity, and permission to consume are separate decisions. Keeping an upload private until all three are resolved is an application design choice, not something a successful PUT response decides for you.

Official references

Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article