Supabase RLS Bypass Testing for Bug Bounty Hunters
Supabase puts Postgres Row Level Security in front of a generated REST API. When RLS is off, every row is one anon or authenticated key away. When RLS is on but policies are wrong, the failure mode looks like IDOR with nicer tooling.
Confirm the trust model
From the web or mobile client, grab the project URL and the anon key (public by design). Sign in as user A and user B. Decode both JWTs—look at sub, role, and any custom claims.
Hit PostgREST directly with A's access token and a filter for B's primary key on profiles, orders, or whatever the app loads after login. Empty by policy is good. One row of B's data is the finding. Try Prefer: return=representation on PATCH/DELETE with a canary column you added on your own row first, then point the filter at B.
OpenAPI embeds at /rest/v1/ help you list columns quickly. GraphQL on /graphql/v1 can express the same IDOR with different syntax—do not skip it if the JS client uses it.
Policies fail in boring ways
Common gaps:
- RLS enabled on
profilesbut forgotten onordersormessages USING (true)for authenticated users- Policies that check
user_id = auth.uid()on SELECT but not on UPDATE - Views or
security definerfunctions that bypass RLS storage.objectspolicies that allowbucket_id = 'avatars'without ownership checks- Policies written for
authenticatedwhile the client still queries asanonafter a failed refresh
RPC is easy to overlook. A function exposed under /rest/v1/rpc/get_invoice might run as definer and accept invoice_id without comparing auth.uid(). Call it with B's id while authenticated as A. Edge Functions that use the service role internally and trust a user-supplied uuid from the JSON body recreate the same class of bug outside PostgREST—trace Authorization forwarding.
Service role keys in a public client are critical when present—treat that as credential exposure, not an RLS puzzle. Do not use a leaked service key against production data beyond a minimal proof if the program even allows it; many will want the key reported unused.
Storage, realtime, and migrations
List buckets, then request an object path you observed from user B's network traffic using A's session. Signed URLs that never expire and are world-guessable are a separate note. Realtime subscriptions sometimes mirror SELECT policies; sometimes they lag after a policy fix—worth a quick subscribe test on a canary channel name.
Public GitHub repos for the app often include early migrations with RLS disabled. If production still matches, cite the migration filename alongside live HTTP proof. If production diverged, do not accuse the repo of shipping the live hole.
Keep the proof tidy
Show policy names if you can read them from an open migration repo, otherwise show behavioral proof. Two accounts, one canary string, one failing request. Note whether anon without login already reads the table—that is often worse.
Recommend enabling RLS on every exposed table, explicit TO authenticated policies, denying by default, locking down RPC to security invoker, and never shipping the service role to browsers or apps. Supabase's own RLS guide is the right checklist to cite next to your HTTP traces.