Skip to content

Build1 publisher3 min readPublished

A starts-with key condition lets one signed S3 upload write anywhere under the prefix

Four HackerOne reports cited in a dev.to analysis trace to S3 write scope set wider than the one object an upload needs. Only one of them is the starts-with key condition that turns a signed upload form into write access across a whole prefix.

The Engineer · Build desk

Illustration accompanying A starts-with key condition lets one signed S3 upload write anywhere under the prefix

What happened

  • Shopify 93691 is that report: its signed upload policy matched starts-with $key files/ where an exact object key belonged.
  • Keys such as files/admin.html, files/../etc/passwd and files/customer-data-dump.csv pass a prefix policy alongside the intended UUID-prefixed photo path.
  • A companion Z3 program asks whether any key exists that the policy admits but the application's own key generator never produces.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • contradiction The case for a repeated starts-with habit rests on one report; the other three failed through policy mutation, bucket-wide scope or public read/list/write, so a prefix-only fix would have left them open.
  • exposure Anyone holding a single signed form under a prefix policy can put HTML on the asset host or overwrite an unrelated object under files/.
  • constraint Exact-key binding makes the server mint the full object key before it signs, so an upload flow can no longer let the browser choose its own path.
  • decision Scanners that inspect only bucket configuration need a separate check on the policies the application signs, because the flaw sits in that contract.

Everything in the example policy is pinned except one field. The bucket is fixed to acme-uploads, the ACL is forced to private, and content-length-range caps the body at 10485760 bytes, or 10 MiB [8][9]. The key gets a prefix match on `files/` [8]. A signed POST lets a browser send bytes straight to S3 without routing them through the application's servers [21]. Once the bytes skip the servers, the signed policy is where the application states which object they may become.

According to the write-up, each extra key the prefix admits hurts a different consumer [11]. An `admin.html` under `files/` gets parsed by whatever serves `assets.acme.example/admin.html`. A key containing `../` passes the policy's string match and becomes a problem for any downstream code that joins keys onto a filesystem path. A `customer-data-dump.csv` key can overwrite an unrelated object [11].

The post's author argues that all four HackerOne reports [1] share one operational shape: "an upload feature, a hurry, a policy that works because it doesn't reject valid uploads and silently accepts every other key under the prefix" [20]. By the post's own summaries, only Shopify 93691 used starts-with on the key [2][7]. Shopify 98819 paired authenticated S3 read with a broad policy-write scope that anyone authenticated could mutate [3]. BCM 764243 had bucket-level write scope broad enough to turn one foothold into a supply-chain implant point [4]. Shopify 94502 exposed several buckets with public read, list and write [5]. Three of the four are Shopify reports [6].

The recommended fix is one line of server code and one condition. The server builds the key as `files/{user_id}/{uuid4}/{filename}` and signs `{"key": key}` for that object alone [12]. The post frames the difference as "prefix policies admit a set of keys; exact policies admit a single key" [14]. The generated key still ends in the client's filename [12]. Exact matching bounds the key to one string, but a filename containing `../` would still reach any downstream path join, so that segment needs sanitising before the server interpolates it.

The detection work is careful. Stave models the signed policy as its own asset, type `s3_upload_policy`, on the grounds that the vulnerability lives in the policy contract and not the bucket [15]. An `allowed_key_mode` field records the verdict, prefix or exact, and `allowed_prefix` or `allowed_key` holds the evidence [15]. The CEL rule behind CTL.S3.WRITE.SCOPE.001 checks the asset type, a write operation and prefix mode [16]. The author notes that CEL only answers yes or no. Finding which keys an attacker can reach takes a search over the key space, and that is where the post brings in Z3 [17].

Z3 is a lot of machinery for a bug whose fix is one dictionary entry. The query still asks what a reviewer wants to know, because a satisfying answer is a concrete key that can be tested [18]. In the post, the model encodes three named witness keys as integer constants [19]. The solver is choosing among three hand-written strings, so it confirms those three are reachable without enumerating the rest of `files/`.

What to watch

  • Z3 output from the Stave example that lists admitted-but-unintended keys beyond its three hand-picked witnesses.
  • The full HackerOne disclosures for Shopify 98819, BCM 764243 and Shopify 94502, which would show whether any also relied on a starts-with key condition.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories