Skip to content
LeakWatch

Guide

How to check Supabase RLS

Row level security is on when an unauthenticated request with the anon key cannot read private tables. A table that answers that request is open, even if the app looks fine when you are logged in.

The Supabase dashboard can show RLS as enabled while a policy still allows everyone. The check that matters is a request that sends only the anon key, the same request a stranger can make from the browser. LeakWatch does that once per discovered table, with limit=1, and throws the row away.

A row coming back means the policy is missing or allows a public read. LeakWatch marks that critical and keeps the table name. HTTP 200 with no row is still an open endpoint and is marked high. If every table refuses the key, the row-access check passes. The anon key itself stays an info note, because it is designed to be public.

In the SQL editor, enable row level security on each listed table and add a policy that matches who should read the row. For a user's own records that is often auth.uid() = user_id. Then repeat the anon-key read. It should fail.

A service_role JWT or an sb_secret key in the JavaScript is a separate problem. Either key bypasses RLS. Remove it from the client and rotate it. LeakWatch does not use those keys to query the project. Check a URL you are allowed to test. LeakWatch is not affiliated with Supabase.

Questions

How does LeakWatch check row level security?
If the page contains a Supabase URL and an anon or sb_publishable key, LeakWatch asks PostgREST for table names and then sends GET with limit=1. It keeps the table name, the column names, and at most one example row with every value masked. It never uses a service_role JWT or an sb_secret key for that request.
Does an empty table mean RLS is on?
No. HTTP 200 with zero rows still means the anon key was allowed to ask. LeakWatch reports that as medium. A returned row with personal or payment columns is critical. Any other returned row is high. A refusal, or no response, is a pass for that check.
What should the policy look like?
Enable row level security on each private table and add a policy that requires auth.uid(), or a tighter rule. Do not use USING (true) on tables that hold user data. Confirm the fix with another read using only the anon key.