LEAKCADE

Supabase RLS mistakes that leak your data

Updated 4 min read

Your Supabase anon key ships in your frontend. That's by design. What keeps strangers out of your tables is Supabase RLS (Row Level Security) plus your grants, and AI-built apps often get one of them wrong. Here are the four mistakes we see most, how to spot them, and SQL you can paste.

QUICK CHECKLIST

  • RLS enabled on every table in public
  • No write policy that allows everyone (true)
  • Grants to anon revoked where not needed
  • Service role key only on the server
  • Tested with the anon key while logged out

Why Supabase RLS matters

Supabase's docs say that a table in an exposed schema without RLS is readable and writable by any role that has a grant on it. In March 2025, researcher Matt Palmer scanned 1,645 apps built with Lovable and found 303 exposed endpoints across 170 of them, about 10.3%. The issue became CVE-2025-48757. Lovable disputes the CVE, saying each customer is responsible for protecting their own app's data. Either way, the data in those tables was reachable.

How a table without RLS leaksThe browser has the anon key, calls the Supabase REST API, the table has no RLS, so every row comes back.BROWSERAnon key is inyour JS bundleREST APIGET/rest/v1/todos?select=*NO RLSTable has grants,no row rulesRESULTEvery row, everyuserBROWSERAnon key is in your JS bundleREST APIGET /rest/v1/todos?select=*NO RLSTable has grants, no row rulesRESULTEvery row, every user

Mistake 1: tables created in SQL without RLS

Tables you make in the Supabase Table Editor get RLS on by default. Tables created with SQL, in the SQL editor or in a migration file, don't. If your AI tool wrote the schema as a migration file, check every create table in supabase/migrations for a matching line:

alter table public.todos enable row level security;
Supabase RLS: what Postgres returns for the same request with and without row level securityA browser calls the Supabase REST API with the anon key and a user token. Without RLS, Postgres returns every row of the todos table. With RLS enabled and an owner policy, it returns only the caller's rows.GET /rest/v1/todos?select=*RLS OFFSELECT * FROM todosevery row200 + every user's todosRLS ON + OWNER POLICYsame query as roleauthenticatedpolicy: auth.uid() = user_idonly the caller's rows200 + your todosBROWSERBrowseranon key + user JWTAPISupabase APIPostgRESTDBPostgrestable todosGET /rest/v1/todos?select=*RLS OFFSELECT * FROMtodosevery row200 + everyuser's todosRLS ON + OWNER POLICYsame query asroleauthenticatedpolicy: auth.uid() = user_idonly thecaller's rows200 + yourtodosWEBBrowseranon + JWTAPISupabasePostgRESTDBPostgrestodos
Same request, same keys. Only RLS and the owner policy change the answer.

Mistake 2: policies that say using (true) on writes

RLS is on, but the policy lets anyone update or delete any row. for update to authenticated using (true) still means every signed-up user can edit every row. Scope writes to the owner:

BEFOREEvery user can edit every row
create policy "update" on public.todos
for update to authenticated
using (true);
AFTERUsers edit only their own rows
create policy "own rows: update" on public.todos
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

The same goes for auth.role() = 'authenticated' on deletes. See why any signed-in user can delete your data.

Mistake 3: forgetting grants

Policies don't remove grants. Supabase's docs note that on existing projects a new table in public starts with select, insert, update and delete granted to anon and authenticated. Revoke what signed-out visitors don't need: revoke all on table public.todos from anon;

Mistake 4: the service role key in the browser

The service role (secret) key bypasses RLS completely. If it ends up in client code or a NEXT_PUBLIC_ variable, your policies don't matter. Keep it on the server only and rotate it if it ever shipped.

How to check your Supabase RLS

  1. List tables without RLS

    Run this in the SQL editor. Every row with false needs attention:

    select tablename, rowsecurity
    from pg_tables where schemaname = 'public';

  2. Enable RLS

    For each of them: alter table public.<name> enable row level security;

  3. Read the policies

    List them with select * from pg_policies where schemaname = 'public'; and look for true in insert, update and delete policies. Views and functions can skip these policies, see security definer views.

  4. Revoke grants anon doesn't need

    Signed-out visitors usually need select on a few public tables at most.

  5. Test while logged out

    Call the REST API with only the anon key and a broad select. Supabase's Security Advisor in the dashboard also flags tables in public without RLS.

Fix prompt you can paste

FIX PROMPT
Go through every table in supabase/migrations. For each one: make sure RLS is
enabled, replace any policy that uses (true) for insert/update/delete with an
ownership check on auth.uid(), revoke grants anon doesn't need, and write a new
migration with the changes. List any table that should stay public read-only.

How Leakcade checks this

Our auth-route check reads your SQL migrations as text. It flags tables that are created without enable row level security and policies that allow writes with (true). It can't see changes made only in the dashboard, so the report tells you when that's the likely explanation.

When we tested 13 public repos, this check flagged 12 open write policies in one Supabase blog starter and one table without RLS in another.

Further reading

  1. Supabase docs: Row Level Security
  2. Supabase docs: Securing your API
  3. Matt Palmer: Statement on CVE-2025-48757
  4. NVD: CVE-2025-48757

FAQ

Is the Supabase anon key secret?

No. It's meant to be public and ships in your frontend. It can only reach what your grants and RLS policies allow, which is why RLS matters.

What happens if RLS is on but there are no policies?

Requests with the anon or authenticated role get no rows back, inserts are rejected, and updates and deletes affect no rows. The table is locked until you add a policy. The service role still has full access.

Does the service role key bypass RLS?

Yes. It ignores every policy, so it belongs only in server code, never in the browser or a public env variable.

Do views respect RLS?

Not by default. A view runs with the rights of the user who created it, which can bypass RLS on the tables underneath. On Postgres 15 and later, create it with security_invoker = true so the caller's policies apply.

Want a second pair of eyes?

We review your whole repo and write a fix prompt for every finding. $19, report within an hour.

Check my repo

Leakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms

Patch Notes by Leakcade

A short weekly email: what changed in AI coding tools, one real security mistake, one prompt you can reuse.