Five Supabase RLS Mistakes a Read-Only Query Can Catch A developer documented five common Supabase Row Level Security misconfigurations — including tables with RLS disabled, policies using `using (true)`, role checks against user-editable `user_metadata`, anon-executable `SECURITY DEFINER` functions, and views without `security_invoker` — that can be detected with read-only queries against Postgres's catalog. The writeup ties the patterns to CVE-2025-48757, a class of bug found across many AI-generated apps where the public `anon` key could read other users' rows, and provides the SQL to find and fix each one. Supabase gives every app a public anon key, and that key can talk to your database through the REST API. Row Level Security is the only thing standing between that key and your tables. When an app gets built fast, sometimes with an AI coding tool writing the migrations, RLS is often on but written in a way that still lets one user read another user's rows. CVE-2025-48757 was exactly this class of bug, across a large number of generated apps. Most of these mistakes are visible in Postgres's own catalog. You can find them with a SELECT , without touching a single row of user data. Here are five, with the query that spots each one and the fix. select n.nspname as schema, c.relname as table name from pg class c join pg namespace n on n.oid = c.relnamespace where n.nspname = 'public' and c.relkind in 'r', 'p' and not c.relrowsecurity; Any row this returns is a table the anon key can read and write in full, as long as the role has the usual grants. Fix: alter table public.profiles enable row level security; Then add policies. With RLS on and no policies, the table returns nothing, which is safe but breaks the app, so people sometimes "fix" it with the next mistake. true create policy "anyone can read" on public.profiles for select using true ; This turns RLS back into "everyone sees everything". Find them: select schemaname, tablename, policyname, cmd from pg policies where schemaname = 'public' and qual = 'true' or with check = 'true' ; Sometimes this is intentional a public list of blog posts . On profiles , orders or anything with personal data it usually isn't. The fix ties the row to the caller: create policy "read own profile" on public.profiles for select using select auth.uid = user id ; user metadata for roles This one looks reasonable and is not: create policy "admins read all" on public.orders for select using auth.jwt - 'user metadata' - 'role' = 'admin' ; user metadata is editable by the signed-in user. Any user can call supabase.auth.updateUser { data: { role: 'admin' } } , refresh their session, and the policy now lets them read every order. Supabase's docs call this out, but it still shows up because the field name sounds authoritative. Find policies that read it: select schemaname, tablename, policyname from pg policies where schemaname = 'public' and qual ilike '%user metadata%' or with check ilike '%user metadata%' ; Fix: keep roles in app metadata only the service role can write it or, better, in your own table such as memberships user id, org id, role that users cannot insert into themselves. SECURITY DEFINER functions the anon key can call Functions in public are exposed at /rest/v1/rpc/