Lovable and Supabase RLS: is your database public?
The gap behind CVE-2025-48757. Your anon key is public by design — RLS is the only thing standing between it and your user table.
In a Lovable app the browser talks to Supabase directly. There is no server of yours in the middle to check permissions, so the database has to check them itself. That mechanism is Row Level Security, and when it is missing, your tables are simply public.
What Lovable does differently
Prompt Lovable for a dashboard and it will generate client-side Supabase queries — supabase.from('profiles').select('*') — running in the visitor's browser with the anon key. That key is published in your bundle on purpose. It is not a secret and was never meant to be one.
This is the crucial mental shift: the anon key is not a password, it is a doorbell. What decides whether the door opens is your RLS policy. Lovable will happily generate the query without generating the policy.
Check it yourself, two minutes
Read your own database as an anonymous stranger
- 1Open your deployed app, press F12, open the Network tab, reload.
- 2Click any request going to *.supabase.co and copy the apikey header value.
- 3Copy your project URL from the same request.
- 4Run the command below against a table you know holds user data.
- 5Repeat for every table name you can find in the app — profiles, users, orders, messages, subscriptions.
The whole exploit is one request
curl "https://YOUR-PROJECT.supabase.co/rest/v1/users?select=*&limit=5" \ -H "apikey: YOUR_ANON_KEY" \ -H "Authorization: Bearer YOUR_ANON_KEY"
- Returns [] — RLS is on and denying. Good.
- Returns rows — every record in that table is public to the entire internet, right now.
- Returns a message about the relation not existing — that table name is wrong, try another.
You can also list every table the key can see at once: request /rest/v1/ on your project and Supabase returns an OpenAPI document naming them. That is what an attacker does first, and it takes about five seconds.
Fix it
The correct end state is: RLS enabled on every table, and a policy that ties each row to the user who owns it. A table with RLS on and zero policies denies everything — that is the safe default to fall back to when you are unsure.
What a correct policy looks like
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; CREATE POLICY "own profile only" ON profiles FOR SELECT USING ((select auth.uid()) = user_id);
Go through every table in my Supabase project and fix Row Level Security. For each table, in this order: 1. Run: ALTER TABLE <table> ENABLE ROW LEVEL SECURITY; 2. List the current policies. Any policy whose USING clause is literally true, or has no condition tied to the current user, is not protection — flag it and remove it. 3. Add a policy scoped to the signed-in user. For owner-owned rows that is USING ((select auth.uid()) = user_id). Wrap auth.uid() in a subselect so Postgres evaluates it once per query instead of once per row. 4. For tables the browser should never read directly (billing, audit logs, admin settings), enable RLS and add no policy at all. When you are done, show me a table of: table name, RLS on/off, policy count, and whether an anonymous user can still SELECT from it.
This is one check out of 40+
Paste your site address and we run the whole list from the outside — leaked keys, open databases, unprotected pages — then hand you one prompt that fixes what we find. Free, about 30 seconds, no signup.
Check my site — free