deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Broad S3 signed upload policies let browsers write to any key under the prefix

A dev.to analysis ties four HackerOne reports to one root cause — starts-with key conditions in signed S3 POST policies — and shows the exact-key fix with solver-verified proof it closes the bug class.

Broad S3 signed upload policies let browsers write to any key under the prefix

The pattern behind four HackerOne reports

Signed POST policies let a browser upload a file straight to S3 without the bytes passing through an application server. The server signs a policy document, the browser submits the file together with that signature, and S3 enforces whatever the policy permits. According to a dev.to analysis, the security of the whole exchange rests on one line of that document: the condition constraining the object key.

The post surveys four HackerOne reports that it says share the same operational shape:

  • Shopify 93691: a signed upload policy matched keys with starts-with $key files/ rather than pinning a single exact key.
  • Shopify 98819: authenticated S3 read access was paired with a broad policy-write scope, and the policy itself could be changed by any authenticated user.
  • Shopify 94502: several buckets were exposed with public read, list and write across multiple keys.
  • BCM 764243: a bucket-level write scope was wide enough that a single point of access became an implant location for a supply-chain attack.

Why a prefix breaks the model

With ["starts-with", "$key", "files/"] in the policy, every key beginning with files/ is fair game. The application expects something like files/abc-uuid/photo.png, but the condition just as happily accepts:

  • files/admin.html, which will be served by whatever fronts the bucket as a static asset
  • files/../etc/passwd, harmless to S3 itself but risky if downstream code naively joins the key into a filesystem path
  • files/customer-data-dump.csv, an unrelated object an attacker can overwrite

The dev.to post draws the line simply: a prefix condition authorizes a family of keys that is hard to bound, while an exact match authorizes exactly one — and a single key is trivial to reason about.

The remediation

Two edits in the upload controller close the gap. First, the server constructs the entire key itself, embedding the user ID and a fresh UUID, as in files/{user_id}/{uuid}/{filename}. Second, the policy replaces the prefix condition with an exact match on that key, {"key": key}, while keeping constraints such as content-length-range. Each signed form then authorizes one specific object chosen server-side, and nothing else. The post's invariant is blunt: every signed S3 upload policy must bind to an exact object key, not a prefix.

From detection to proof

The analysis then moves from fix to tooling. The Stave framework discussed in the post models the upload policy as its own asset type, separate from the bucket, on the grounds that the vulnerability lives in the policy contract rather than in bucket settings. A control named CTL.S3.WRITE.SCOPE.001, rated high severity, flags any write-mode policy operating in prefix mode.

That control is a state assertion: it says the configuration is unsafe but not what an attacker actually gains. The author therefore encodes the follow-up question in the Z3 solver — does any key exist that the policy admits yet the application's key generator would never produce? Three witness keys stand in for the space: an intended UUID-based key, files/admin.html, and a traversal-style key. In prefix mode the solver returns SAT, with files/admin.html as a concrete counterexample. After the exact-key fix it returns UNSAT, meaning no admitted key falls outside the intended set, so the remediation provably removes the whole class of problem rather than a single instance.

The broader methodological claim: rule engines such as CEL answer whether a configuration is permissive, while solver witness extraction says which specific keys it is permissive about. A concrete witness is what turns a vague finding into demonstrable impact in a report. Extending the approach to another attack class, such as writing under a different tenant's prefix, means writing a new predicate rather than a new program.

Why it matters

Direct-to-S3 uploads are a default pattern across stacks, and this mistake is one line in an ordinary Rails, Express or Django controller. Functional tests will not surface it, because legitimate uploads keep succeeding; the policy simply fails to reject everything else under the prefix. The blast radius is genuine, as the cited reports show — overwritten objects, cross-feature writes and a supply-chain compromise. The fix is cheap and mechanical: generate the key server-side and bind the policy to it exactly. And the two-step methodology travels well beyond AWS — separate the question of whether a configuration is unsafe from the question of what an attacker can actually reach, because the second answer is what makes a finding actionable.

  • #aws
  • #s3
  • #security
  • #cloud-storage
  • #devsecops

Related posts