Secure direct uploads with AWS S3 presigned URLs

Design browser-to-S3 uploads with short-lived permissions, server-owned object keys, validation, completion checks, and cleanup.

AWSAmazon S3UploadsSecurityCloud Storage
Secure browser upload flow through a PHP or Node backend to Amazon S3

Presigned URLs can keep large upload bodies away from the application server, but they should not give a browser control over permanent object names or unrestricted bucket operations.

Let the application choose the object key

Authorize the user, create an upload record, and generate a random server-owned key under a constrained prefix. Sign only the required operation with a short expiry and expected content constraints where the SDK and workflow support them.

A durable upload state machine

Create a pending upload row before returning credentials. Store its owner, random object key, intended purpose, expected maximum size, declared media type, expiry and state. Useful states include pending, uploaded, validating, accepted, rejected and expired. The object key should not contain an untrusted path or become public merely because the upload completed.

A server-owned key such as a random identifier under a tenant prefix prevents one user from choosing another user's object name. Keep the original filename separately for display after removing control characters and path components. Never use it as the authorization decision.

Choose PUT, POST or multipart intentionally

A presigned PUT is simple for one object and lets the client send the body directly. A presigned POST policy can constrain key prefixes, content length and selected form fields. Multipart upload is appropriate for large files and unreliable networks but adds part tracking, completion and abandoned-upload cleanup. Choose based on the product's maximum size and retry needs, not only on the shortest sample code.

Whatever method is chosen, bind the signature to the narrowest practical operation. Keep expiry short enough to reduce exposure but long enough for the expected connection and file size. Do not place broad AWS credentials or an unrestricted bucket policy in browser code.

Treat browser metadata as a claim

File names, MIME types, and sizes supplied by the browser are not final proof. After upload, inspect S3 metadata and, for sensitive workflows, process the object through malware scanning or media validation before making it available.

Validate content in quarantine

Upload to a private quarantine prefix. A validation worker can inspect actual size, detect type from bytes, decode media, enforce dimensions or duration, scan for malware and remove dangerous metadata. Only accepted content should be copied, moved logically or marked available to the delivery layer.

Parsing complex formats is itself a security boundary. Keep image and media tools patched, apply CPU and memory limits, and reject decompression bombs or unreasonable dimensions. A successful S3 upload proves storage, not safety.

Use an explicit completion step

The client should notify the application after S3 accepts the object. The server verifies the key belongs to the pending upload and checks size and state before attaching it to a business record. Repeating the completion request should be idempotent.

  1. The authenticated client requests an upload for a declared purpose.
  2. The application authorizes that purpose and creates a pending database record.
  3. The application returns short-lived scoped upload data.
  4. The browser uploads directly to S3 and reports completion using the upload ID, not an arbitrary key.
  5. The server performs a HEAD request and starts validation.
  6. The final business record references the object only after validation succeeds.

S3 event notifications can complement the completion call, especially when a browser closes after uploading. They are delivered asynchronously and may repeat, so consumers also need idempotency. Reconcile pending database rows with actual objects instead of relying on either signal alone.

IAM, encryption and browser configuration

The signing role should have only the required actions on the constrained bucket and prefix. Block public access unless the architecture explicitly requires public objects. Use bucket-owner enforced settings where appropriate, encrypt at rest, and restrict cross-origin rules to the actual application origins, methods and headers.

CORS is not authorization. It controls browser access to responses, while the signature and IAM policy control S3 operations. Test the production origin, including HTTPS and any subdomain, and expose only response headers the client genuinely needs such as an entity tag for multipart bookkeeping.

Delivery is a separate decision

Private accepted objects can be delivered through short-lived signed URLs, an authenticated application route, or a CDN with signed access. Use content-disposition and content-type headers suitable for the file. For user-controlled documents, downloading as an attachment from an isolated domain can reduce script-execution risk.

Clean up abandoned work

Apply lifecycle rules or a scheduled cleanup to pending prefixes. Keep the bucket private, serve approved objects through controlled URLs or a CDN policy, and log which application identity initiated each upload.

Operational signals

  • Count requested, completed, validated, rejected and expired uploads.
  • Measure time from credential issue to upload and from upload to acceptance.
  • Alert on growing quarantine age, validation failures and incomplete multipart uploads.
  • Track storage by state and tenant so abandoned data cannot grow unnoticed.
  • Audit who requested, completed, approved or deleted an object without logging signed URLs.

Threat-focused testing

Try an oversized body, a different content type, a modified key, an expired signature, a reused upload ID, another user's ID, concurrent completion calls and an object replaced between checks. Verify rejected content never becomes deliverable. Simulate a browser going offline after upload and ensure reconciliation reaches one final state.

Direct-to-S3 upload is a performance architecture, not a shortcut around application security. The application still owns identity, naming, lifecycle and the decision that an uploaded object is safe to use.

Document those ownership boundaries for support and incident response. An operator should be able to move from an application upload ID to its object, validation attempts and final business record without copying a signed URL into tickets. That trace makes deletion, privacy requests and storage reconciliation safer as the system grows.

Discuss a project