Finding 01 · cross-tenant read
Your records answer to anyone
The classic. Swap an ID in the URL and the database hands over a row belonging to a different customer.
GET /rest/v1/orders?id=eq.4471
as: anonymous
200 · 1 row · owner: another tenant
The fix we hand youUSING (auth.uid() = user_id)
Finding 02 · open write
A stranger can edit your data
Worse than reading. We check whether an account that owns nothing can create, overwrite or delete. The check runs inside a transaction the database is asked to roll back.
PATCH /documents/inv_88
as: anonymous
200 · write accepted · rolled back
The fix we hand youallow write: if request.auth.uid == resource.data.ownerId;
Finding 03 · no rules at all
A table nobody wrote a policy for
Row-level security never switched on, or a Firestore collection with no matching rule. Not a subtle mistake. The boundary was never created.
table: public.profiles
rls: disabled
every row readable by the anon key
The fix we hand youALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
Finding 04 · exposed bucket
Uploads anyone can list
Avatars, invoices, ID scans. Buckets are the part a builder rarely gets to, and a listable one gives away every filename inside it.
GET /storage/v1/object/list/avatars
as: anonymous
200 · 1,284 objects listed
The fix we hand youpublic = false, plus a per-owner storage policy
Finding 05 · leaked key
The admin key is in your bundle
A service-role key shipped into the JavaScript every visitor downloads. It bypasses every rule you have, so nothing else on this list matters until it's rotated.
/assets/index-4f2c.js
match: service_role JWT
bypasses RLS entirely
The fix we hand yourotate it, move it server-side, ship the anon key only
Finding 06 · auth misconfig
Anyone can mint an account
Anonymous sign-in left switched on, or open self-registration. Every rule that trusts "is signed in" now trusts the whole internet.
POST /auth/v1/signup
email confirmation: off
session issued instantly
The fix we hand yougate on ownership, not on the existence of a session