Skip to main content

The upload door that opened onto everyone's room

Metadata

  • Title: The upload door that opened onto everyone's room
  • Source of the case: Real HackerOne report #254200 (Unikrn)
  • AWS service(s): S3 (presigned POST upload policy)
  • Risk archetype: Latent broad write — a syntactically valid upload policy whose key scope spans every tenant instead of one
  • One-line hook: Can you prove this upload policy keeps one user out of another user's files?

0. The challenge (what the reader does first)

Scenario given to the reader:

A gaming platform lets users upload files — avatars, clips, attachments — by handing each authenticated user a presigned POST policy for the unikrn-user-uploads bucket. The policy is well-formed and unexpired. Its only key constraint is a starts-with condition on $key. You have the policy below and nothing else.

Evidence they're handed (and nothing else):

{
"bucket": "unikrn-user-uploads",
"upload_policy": {
"conditions": [["starts-with", "$key", "uploads/"]],
"expiration": "2024-12-31T00:00:00Z"
}
}
  • The JSON export above. No AWS credentials. No live account. No scripts.

The questions they must answer from the evidence alone:

  1. What set of object keys does this policy actually permit a holder to write — is it scoped to one user, or to every key under uploads/?
  2. No cross-tenant write has happened yet, but the prefix covers all tenants — what is the latent blast radius the moment any user decides to set $key to uploads/<someone-else>/...? (The exposure is structural; it can be triggered at any time, by any holder, with no further misconfiguration.)
  3. Which path makes this exploitable — the starts-with $key, "uploads/" condition being broader than a single tenant?
  4. Is there a second constraint anywhere in the policy (a per-user value, a stricter condition) that re-narrows the scope, or is the prefix the only gate?
  5. What single rule would have forced every issued upload policy to scope $key to the requesting user's own prefix?

1. The manual problem

To answer by hand you have to read the policy the way S3 enforces it: the only thing standing between this user and the whole namespace is starts-with $key, "uploads/". That looks like a restriction — it is a prefix, after all — and the eye reads "restricted to uploads" and moves on. The trap is that uploads/ is the root of every user's space, not one user's. To see the bug you have to know the application's intended layout (one subfolder per user under uploads/) and realize the policy never encodes that layout. Worse, there is nothing dangerous in the snapshot yet — no malicious object, no cross-tenant write on record. The vulnerability is purely in what the policy allows, which means inspecting current state tells you nothing; you have to reason about the reachable state. There is no clean way, by hand, to prove "every holder of this policy can already reach every other holder's files."


2. The reasoning wall

What they hitWhat they said / would say
starts-with uploads/ looks like a scope, so it reads as safe"There's a prefix condition right there. I saw a constraint and assumed the constraint was per-user."
Nothing bad has happened, so nothing flags"I went looking for a breach. There wasn't one. The policy was just... waiting. How do you flag a thing that hasn't gone wrong yet?"
The bug is in reachable state, not current state"We can audit what's in the bucket. What I actually needed to know is what anyone could put there, and where."

The insight the reader should reach on their own:

A prefix is only isolation if it is per-tenant; a shared prefix is a latent breach that any holder can trigger at will, and proving it means reasoning about reachable keys, not the keys that exist today.


3. Why scanners miss or flatten it

A per-setting scanner can confirm the upload policy is syntactically valid: conditions are well-formed, the expiration is in the future, $key is constrained by a starts-with. Every check passes, because every individual field is correct. What the scanner cannot do is reason that uploads/ is a tenant-shared prefix and therefore violates the isolation invariant the application depends on. To the scanner, starts-with $key, "uploads/" and starts-with $key, "uploads/${user_id}/" are the same shape — a non-empty prefix condition — and both look fine. It has no model of "one tenant per subfolder," so it cannot tell a scope that isolates from a scope that merely looks scoped. The specific thing it cannot see is that this valid policy lets any tenant write under any other tenant's path. That is a statement about the relationship between the prefix and the tenancy model, not about any one field.


Pivot point. Everything above is the gap. Everything below is Stave filling it. The reader has now done the work and hit the wall. Only now does the tool appear.


4. The evidence Stave consumes

The same static export the reader had — no new privileges, no live cloud:

{
"bucket": "unikrn-user-uploads",
"upload_policy": {
"conditions": [["starts-with", "$key", "uploads/"]],
"expiration": "2024-12-31T00:00:00Z"
}
}

Stave normalizes this into an observation snapshot: an upload-policy asset whose key-scope condition and tenancy model are both evaluable facts, so the predicate can compare the granted prefix against the per-tenant boundary the application declares.


5. The reasoning Stave performs

  • Control / invariant: CTL.S3.TENANT.ISOLATION.001 — a presigned upload policy must scope $key to a single tenant's prefix; a tenant-shared prefix is a violation.
  • What it evaluates: The control reads the starts-with $key condition and tests whether the granted prefix is bounded by a per-tenant component (e.g. uploads/${user_id}/). A prefix that stops at uploads/ — shared by all tenants — fails, because the policy permits writes across tenant boundaries. It reasons over reachable keys, not stored keys, so it fires even though no cross-tenant write has occurred yet.
  • Verdict produced: The control fires NON_COMPLIANT on the latent over-broad scope. If the tenancy model is unknown rather than provably per-tenant, the control reports the finding rather than assuming isolation holds.
Issue: unikrn-user-uploads — upload policy prefix is tenant-shared

CTL.S3.TENANT.ISOLATION.001 NON_COMPLIANT
asset: s3://unikrn-user-uploads (presigned upload policy)
evidence: condition starts-with $key "uploads/" is shared by all tenants;
no per-tenant component (expected uploads/${user_id}/)
verdict: any policy holder can write under any other tenant's prefix —
latent cross-tenant write, exploitable at will

security_state: NON_COMPLIANT

6. The prevention artifact Stave produces

  • Artifact: A corrected presigned POST policy whose starts-with $key condition is scoped to the requesting user's own prefix, issued per-tenant at signing time.
  • What it forecloses: The latent state from question 2 — any holder setting $key to another user's path. Once the condition is bound to uploads/${user_id}/, the signed policy mathematically cannot authorize a write outside the requester's own folder, so the cross-tenant write is unreachable rather than merely unobserved.
# Corrected presigned POST policy — per-tenant key scope
{
"expiration": "2024-12-31T00:00:00Z",
"conditions": [
{"bucket": "unikrn-user-uploads"},
["starts-with", "$key", "uploads/${user_id}/"],
["content-length-range", 0, 10485760]
]
}
# ${user_id} is substituted from the authenticated session at signing time,
# so each issued policy admits only that user's own prefix. A request with
# $key = "uploads/<other-user>/evil.png" no longer satisfies the condition
# and S3 rejects the upload.

7. What the team no longer does manually

BeforeAfter Stave
Read each upload policy and mentally compare its prefix against the intended per-user layoutOne control proves the prefix is per-tenant or flags it deterministically
Try to assess risk by auditing what is already in the bucketThe control reasons over reachable keys, catching the latent breach before any object exists
Hope every code path that signs a policy remembered to append the user prefixA corrected per-tenant policy template makes the scoped prefix the only issuable shape

Positioning line for this case

Stave proves that unikrn-user-uploads hands every user an upload policy reaching every other user's prefix, proves the cross-tenant write is reachable today even though none has happened, and emits the per-tenant policy that makes it unreachable.


Reuse checklist

  • A reader could attempt section 0 with zero Stave knowledge
  • Stave is not named or shown before the pivot point
  • Section 2 quotes are real (or honestly plausible), not slogans
  • Section 3 names the specific thing per-setting tools can't see
  • Section 6 closes the exact latent state raised in section 0, question 2
  • The title names the failure, not the product