skip to content

Presigned URLs & Browser Uploads

A presigned URL lets a client GET or PUT exactly one object, carrying the signer's permissions plus an expiry instead of any credentials. Interviewers love it as the answer to 'how do users upload a 2 GB file without it passing through my API?'

part ofAWSoverview, primer and where to startread it →
on this pageshow

questions

5

What is an Amazon S3 presigned URL, and whose permissions does the person holding one actually exercise?

level: juniorimportance: must knowfreq 78%

answer

  1. no AWS credentials reach the client
  2. signature and expiry live in the query string
  3. one method, one bucket, one key
  4. the signer's permissions, checked at request time
  5. a bearer token with a countdown

basics

~20 s

An S3 presigned URL is an object URL carrying a SigV4 signature and an expiry in its query string. Whoever holds it performs one specific operation on one specific object using the signing principal's permissions, with no AWS credentials of their own.

solid answer

~40 s

A presigned URL is a normal S3 object URL with SigV4 authentication moved into the query string — `X-Amz-Credential`, `X-Amz-Date`, `X-Amz-Expires`, `X-Amz-SignedHeaders` and `X-Amz-Signature`. I generate it server-side using credentials that already hold `s3:GetObject` or `s3:PutObject` on that key, then hand the URL to a browser or partner that has no AWS identity at all. S3 verifies the signature, checks the expiry, and evaluates the request as though my signing principal had made it, so the holder can never do more than the signer could. The signature covers the HTTP method, bucket, key and any signed headers, so one URL is good for exactly one operation on exactly one object. That is how a multi-gigabyte upload goes straight from the browser to S3 instead of streaming through my API servers.

go deeper

for a junior

Be able to say plainly that a presigned URL lets a browser read or write one object directly, that the signature and expiry ride in the query string, and that no AWS credentials go to the client.

for a middle

Explain that the signature covers method, bucket, key and signed headers, and that S3 evaluates the request against the signing principal at request time — so removing the signer's permission invalidates outstanding URLs.

for a senior

Show the production discipline: the server derives the key, expiries are minutes not days, the signing role is scoped to one prefix, and you treat the URL as a leakable bearer token in logs and referrers.

for a principal

Own the tradeoff between direct-to-S3 transfer and proxying through your own tier — bandwidth cost and latency against the loss of a single inspection point for validation, virus scanning, quotas and audit.

## The problem it solves A browser or a partner system needs to read or write one object in a private S3 bucket. The bad answers are to make the bucket public, to ship AWS access keys to the client, or to proxy every byte through your own API. Proxying costs you bandwidth, request latency and memory, and it turns a 2 GB upload into a 2 GB problem for your application servers. A presigned URL removes all three problems: the client talks to S3 directly, and the only thing you gave away is a time-boxed permission to do one thing. ## What the URL actually contains Signature Version 4 normally authenticates a request through an `Authorization` header. Presigning moves the same material into the query string so a plain browser navigation or `fetch` can carry it: ``` https://my-bucket.s3.eu-west-1.amazonaws.com/reports/2026-q1.pdf ?X-Amz-Algorithm=AWS4-HMAC-SHA256 &X-Amz-Credential=AKIA.../20260321/eu-west-1/s3/aws4_request &X-Amz-Date=20260321T101500Z &X-Amz-Expires=900 &X-Amz-SignedHeaders=host &X-Amz-Signature=9f86d0... ``` The signature is an HMAC over a canonical form of the request: the HTTP method, the path (bucket and key), the canonical query string, the list of signed headers, and a payload hash — which for presigned URLs is the literal `UNSIGNED-PAYLOAD`, because the signer does not have the body. Change any signed element and the signature no longer matches; S3 answers `403` with `SignatureDoesNotMatch`. Two consequences fall straight out of that. First, the URL is scoped to one method and one key: a URL presigned for `GetObject` on `reports/2026-q1.pdf` cannot fetch a different key, cannot list the bucket, and cannot be turned into a PUT. Second, if you sign a header — `Content-Type`, say — the client must send exactly that header value or the request fails. ## Where the permissions come from This is the part interviewers press on. A presigned URL grants nothing by itself. When the request arrives, S3 identifies the signing principal from `X-Amz-Credential`, verifies the signature and expiry, and then runs the normal authorization evaluation **as if that principal had made the request directly**: identity policy, bucket policy, any relevant Organizations or endpoint policy. So: - If the signing role cannot `s3:GetObject` that key, the presigned URL fails too. You cannot presign your way past a permission you do not have. - If permissions are removed after signing, the URL stops working — evaluation happens at request time, not at signing time. - An explicit `Deny` in the bucket policy still wins. Block Public Access does not interfere, because a presigned request is an authenticated request, not an anonymous public one. - Conversely, a badly scoped signer is dangerous: if you sign with an admin role and a bug lets a caller choose the key, you have handed out admin-shaped reads. Because the credentials are the signer's, the URL is a **bearer token**. Anyone who obtains it — from a log line, a chat message, a browser history entry — can replay it until it expires. Treat it as a secret with a countdown. ## Generating one Every SDK exposes it, and the operation is purely local: no API call to S3 is made when you presign, which is why it is fast and works offline. ```python url = s3.generate_presigned_url( "put_object", Params={"Bucket": "my-bucket", "Key": "uploads/abc123.png"}, ExpiresIn=900, ) ``` The CLI's `aws s3 presign s3://my-bucket/reports/2026-q1.pdf --expires-in 900` produces a GET URL; other methods come from the SDKs. ## The interview shape The canonical question is "how does a user upload a large file without it passing through your API?" The expected answer: the client asks your API for a presigned PUT URL; your API authenticates and authorizes *the user*, decides the object key itself (never letting the client dictate it), signs a short-lived URL, and returns it; the browser PUTs the bytes straight to S3. Your API sees a few hundred bytes of JSON instead of gigabytes of body. The same shape in reverse — a short-lived presigned GET — serves private downloads without making the bucket readable. What a presigned URL is *not*: it is not a way to grant a client ongoing access, not a substitute for authenticating the user in your own API, and not something you can hand out with a year-long expiry and forget about.

  • If the signing role loses s3:GetObject an hour after you hand out a URL that is valid for six hours, what happens?
    The URL stops working immediately. Authorization is evaluated when the request arrives, not when the URL is signed: S3 verifies the signature and expiry, then runs the normal policy evaluation against the signing principal. Losing the permission — or an explicit deny appearing in the bucket policy — kills every outstanding URL signed by that principal for that key.
  • Your API lets the caller supply the object key it wants presigned. Why is that a problem?
    You have turned your signing role's permissions into an open API. A caller can ask for a key belonging to another tenant and get a valid URL, because the signature is produced from whatever key you passed in. The server must derive the key itself from the authenticated user — a tenant prefix plus a generated identifier — and never echo back a client-chosen path.
  • Does generating a presigned URL call S3?
    No. Presigning is a local cryptographic operation: the SDK builds the canonical request and HMACs it with the credentials it already holds. Nothing is registered server-side, which is why signing is instant, works without network access, and also why there is no list of issued URLs to revoke later.

saying these in an interview costs you the question

  • Says the URL makes the object public to everyone forever
  • Thinks the client needs its own IAM user or keys
  • Claims a presigned URL bypasses bucket policies and explicit denies
  • Believes one URL can be reused for other keys in the bucket
  • Assumes authorization is checked at signing time, not request time

context

open as a page

Code running in AWS Lambda generates an S3 presigned GET URL with a 24-hour expiry, but recipients start getting 403 responses a few hours later. Why does the URL die early, and what is the real ceiling on a presigned URL's lifetime?

level: middleimportance: should knowfreq 55%

basics

~20 s

Two clocks govern a presigned URL, and the shorter wins. Lambda signs with the execution role's temporary STS credentials, so the URL dies when that session expires, no matter what X-Amz-Expires says. SigV4 itself caps the requested expiry at seven days.

open as a page

You let browsers upload directly to S3. With a presigned PUT URL, what stops a client from writing a key you did not intend or pushing a 5 GB file, and what does a presigned POST policy give you that the PUT does not?

level: middleimportance: should knowfreq 46%

basics

~20 s

A presigned PUT pins the bucket, key and method into the signature, so the client cannot redirect the write — but it cannot bound the body size. A presigned POST signs a policy document whose conditions, including content-length-range, are enforced by S3 on upload.

open as a page

A presigned S3 GET URL for a private customer document turns up in a third party's access logs and is still being fetched. Why can you not simply revoke that one URL, and what actually cuts off access?

level: seniorimportance: should knowfreq 50%

basics

~20 s

A presigned URL is a bearer token that S3 never recorded: validity is recomputed from the signature, the expiry and the signer's current permissions on every request. With nothing to revoke individually, you must deny the object, remove the signer's permission, kill the signing session, or move the object.

open as a page

A browser PUT to an S3 presigned URL fails with a CORS error in the developer console, yet the same URL works from curl. What is actually wrong, and what do you configure to fix it?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

Nothing is wrong with the signature — curl proves that. The bucket has no CORS configuration permitting your page's origin and the PUT method, so the browser blocks the cross-origin request. Fix it by putting a CORS configuration on the bucket.

open as a page