Where authorization breaks in serverless backends
A generic, target-free field guide to the access-control failures I keep finding in Supabase / edge-function backends, and the two-account method that surfaces them. No program names; this is about the class, not any one bug.
A note on what this is: every specific finding behind this generalisation is from a private engagement and stays private. What follows is the shape of the class, the kind of thing any researcher or defender can use, with nothing tied to a real program.
The trust boundary moved and nobody guarded it
The classic web app puts authorization in the server: a request arrives, a middleware checks the session, the handler decides what this user may touch. Serverless backends quietly relocate that boundary. Now there are three places authorization can live, and a finding hides wherever a team assumed one of the others was handling it:
- Row-Level Security (RLS) in the database, enforced per-row by policy.
- Edge / serverless functions that run with elevated rights and are supposed to re-check the caller.
- The client, which holds a key and decides what to ask for.
The failure is almost always a seam. RLS is enabled on the tables the UI reads, but an edge function uses a service-role key that bypasses RLS entirely, and forgets to re-implement the ownership check the policy would have done. Every caller of that function now reads every row.
The two-account method
The only reliable way to prove an access-control bug, to yourself and to a triager, is two accounts and a clean diff. One probe with one account proves nothing; the data might legitimately be yours.
- Provision User A and User B, ideally with no relationship: different org, different tenant, B freshly created and owning nothing.
- As A, exercise the feature and capture the object IDs it touches.
- As B, replay A’s requests against A’s IDs. Change only the auth token.
- If B gets A’s data, the object check is missing. If B, a pristine account that owns nothing, gets global data, the function is running with elevated rights and never re-checked the caller.
That last case is the serverless tell. A vanilla IDOR leaks one other user; a service-role function with no ownership check leaks the table.
| T+0 | List edge functions / API routes the SPA calls. Note any that return more than the caller should own. |
|---|---|
| T+3m | Create User B, pristine, no org, no rows, never touched the feature. |
| T+6m | Replay A’s request with B’s token, A’s object IDs unchanged. change only the bearer |
| T+8m | Compare responses byte-for-byte. Same data to B = broken authorization. |
| T+12m | Confirm against a second pristine account before calling it. One account is an anecdote. |
Why I keep this discipline
It is easy to mistake “the API returned data” for “I found a bug.” The two- account diff is what separates a real, reproducible access-control finding from a screenshot of your own data. It also makes the writeup trivial: a triager who sees User B, owning nothing, holding User A’s records doesn’t need to trust your narrative. The proof is in the diff.
That discipline is the actual transferable lesson here, more than any one serverless quirk. The platforms change; “re-derive identity server-side, and prove the bug with a clean second account” does not.