skip to content

When an application server constructs a pre-signed URL (or Azure SAS token) for a client to upload a single file to object storage, which specific parameters should it constrain to keep the resulting credential least-privilege, beyond just setting an expiry time?

level: middleimportance: must knowfreq 65%

answer

  1. scope = path + verb + content, not just time
  2. presigned POST for size/type conditions
  3. write-only beats read+write+delete
  4. stored access policy = server-side revoke handle

basics

~10 s

It should limit the link to one exact file, one action (like upload-only, not delete), and sometimes the file size or type — not just make the link expire soon.

solid answer

~30 s

Beyond expiry, the signer should scope the URL to: (1) a specific object key/path, not a wildcard prefix or the whole container; (2) a specific permission (e.g., PUT only, not GET+PUT+DELETE); (3) optionally content constraints like max size or required Content-Type/Content-MD5 so the client can't substitute an arbitrary payload; (4) optionally an IP-range or HTTPS-only restriction; (5) for Azure SAS, ideally a stored access policy for centralized revocation. Narrowing all of these limits what a leaked URL can do even before it expires.

go deeper

for a junior

Should recognize that a link can be limited to one file and one action, even without naming SAS parameters precisely.

for a middle

Should name path scope, verb scope, and be able to describe at least one content constraint like size or content-type capping.

for a senior

Should discuss the bearer-leak blast radius trade-off across scope dimensions and know stored access policies as a revocation lever.

for a principal

Should evaluate scope design as an organization-wide policy question: standardizing minimal-scope signing helpers, auditing over-broad grants, and choosing SAS types deliberately per use case.

## What least privilege means here Least privilege for a pre-signed URL or SAS token means constraining **every dimension** of what the token can do, not just how long it can do it. Expiry alone bounds the time window of exposure, but during that window, however short, a bearer of the URL can exercise the full permission set baked into it. So an issuer aiming for least privilege has to think, in addition to time, about: - **resource scope** - **permission scope** - **content constraints** ## Resource scope The first constraint is resource scope: a URL should be signed against a single object key or path, such as `uploads/user123/avatar.png`, never a wildcard prefix or the container root, unless the client genuinely needs to write many objects in one session. - Azure SAS tokens support both **service-level SAS** (scoped to one blob) and **account-level SAS** (scoped to potentially every service and container in the account); reaching for account SAS out of convenience is a common over-grant. - AWS pre-signed URLs are inherently scoped to one S3 object per URL by construction of the signature, but careless key-naming logic can still let attacker-influenced input widen effective scope. ## Permission or verb The second constraint is the permission or verb: a token should carry only the operations actually needed. If the client only needs to upload, the signed permission should be **write-only** (SAS sp=w, or S3 signed for `PutObject`), not read+write+delete+list. This matters because the URL is a bearer credential; if it leaks via browser history, a proxy log, or a referrer header sent to a third-party analytics script, the blast radius of that leak is exactly the permission set granted, independent of expiry. | Leaked token | Blast radius | |---|---| | A write-only token that leaks | lets an attacker overwrite that one object | | A read+write+delete token on the same leak | also lets them exfiltrate and destroy data | ## Content shape The third constraint is content shape: many storage APIs let the issuer pin conditions on the body of the request itself. - On S3, a **presigned POST** (as opposed to a presigned PUT) can carry a policy document with conditions like `content-length-range`, capping file size, and a required `Content-Type`, so the client cannot substitute an arbitrarily large payload or spoof the MIME type. - A **presigned PUT** can similarly have `Content-MD5` or `Content-Length` baked into the signed headers, causing the signature to fail if the client sends a different body than the one intended. Without this, a URL scoped correctly to path and verb can still be abused to upload a 50 GB file where a 2 MB avatar was expected, running up storage costs or exhausting quota. A concrete illustration: an e-commerce platform issuing presigned POSTs for product-image uploads pins content-length-range to 10 KB to 5 MB and Content-Type to image/*, so a leaked URL can only be used to upload a small image to that one product's image slot, not to plant an executable or bankrupt the storage budget. ## Network and transport A fourth, often-overlooked constraint is network and transport: both S3 and Azure support requiring **HTTPS-only** requests, and Azure SAS additionally supports a signed IP range restricting which source addresses may present the token, which mitigates, though does not eliminate, the impact of a leaked URL being used from an unexpected location. ## Stored access policies Finally, Azure specifically offers **stored access policies**: instead of baking permissions and expiry directly into the SAS signature, the container stores a named policy with those settings, and the SAS token just references the policy's ID. This doesn't narrow an individual token's scope further, but it changes the revocation story, since permissions live server-side, the issuer can edit or delete the stored policy to invalidate every SAS token issued against it, without needing to rotate the account's master key and break unrelated tokens. ## The common thread The common thread across all these constraints is that a signed URL is not the app server's authorization decision, delayed; it's a **static, replayable credential** that the storage service enforces mechanically with no awareness of your application's business rules. Every dimension you don't explicitly constrain at signing time is a dimension the bearer can freely exploit for as long as the token remains valid. Treating expiry as sufficient is one of the most common mistakes engineers make when adopting this pattern, because it addresses only the how-long axis while leaving how-much wide open.

  • How would you cap the uploaded file's size using a pre-signed URL alone?
    On S3 you use a presigned POST, not PUT, with a policy document that includes a content-length-range condition, or you pin Content-Length/Content-MD5 into a PUT's signed headers. Azure SAS doesn't cap size directly, so teams often pair it with a post-upload check that deletes oversized objects.
  • Why scope to PUT-only instead of also allowing GET on that same key?
    Because if the URL leaks through browser history, proxy logs, or a referrer header, an attacker with GET could exfiltrate the uploaded file's contents rather than just being able to waste a write. Keeping permissions minimal limits leaked-URL impact to whatever verb was actually needed.
  • What's a stored access policy in Azure SAS, and why does it help beyond narrower scoping?
    It's a named policy object saved on the container that a SAS token references by ID. Because permissions and expiry live server-side, revoking is a matter of deleting or editing that stored policy, unlike ad-hoc account SAS tokens, which can only be killed by rotating the whole account key.

Like a hotel keycard programmed to open only your room's door, not the whole floor, and not the minibar unless you've paid for it, and to stop working at checkout time: every extra door or feature left enabled is something anyone who copies the card can use.

saying these in an interview costs you the question

  • Thinks expiry alone is least privilege
  • Grants delete or list permission just in case
  • Doesn't know you can cap size or content-type at signing time
  • Confuses account-level key rotation with per-token revocation
  • Unaware that a leaked URL exposes the full permission bundle for anyone holding it, not just the intended client

context