← Back to blog
SecurityScanaris Team · 8 min read · October 03, 2026

Lovable and the RLS vulnerability (CVE-2025-48757): why your app might be exposing data

CVE-2025-48757 is a critically-rated Lovable vulnerability (CVSS 9.3) that exposed more than 170 applications. The problem: anyone, without logging in, could read —and in many cases modify— data in your database: names, emails, third-party API keys, even payment status. The root cause is not a hidden Lovable bug but a pattern shared by almost every "vibe-coded" app: trusting all database security to Supabase's RLS policies… and leaving them incomplete. This guide covers what exactly happened, how to check in five minutes whether your app has the same hole, how to close it with concrete SQL, and what to do if you have already exposed data.

What actually happened

Lovable builds apps where the browser talks directly to the database (Supabase) through REST calls, using the public anon key. That is a sound design as long as every table has Row-Level Security (RLS) policies deciding which rows each user may read or touch. The vulnerability appeared because many projects shipped with tables that had no RLS or insufficient RLS. An attacker only had to edit the REST request —change a filter in the URL— to ask for "all rows" of a table that should have been protected.

To understand why editing a URL is enough, you need to know how Supabase works under the hood. In front of your Postgres database sits a layer called PostgREST that turns every table into an HTTP endpoint: GET /rest/v1/customers?select=* returns rows from the customers table. The anon key is not a secret: it ships in your app's JavaScript and anyone can see it. The only thing deciding which rows come out of that endpoint is the table's RLS policies. Without them, the answer is the whole table.

It was reported in March 2025 by Matt Palmer and Kody Low, and independently by Danial Asaria, with public disclosure on May 29, 2025 (technical write-up, CVE entry). Any Lovable project using a database created on or before April 15, 2025 could be affected.

Timeline in short

  • March 2025: first private reports to Lovable; published apps are found with tables readable without a session.
  • April 15, 2025: the cut-off date Lovable gives for affected projects; apps generated after it include changes in how tables are created.
  • May 29, 2025: public disclosure and CVE assignment with a critical score.
  • Afterwards: Lovable adds security warnings in the editor, but the policies remain the responsibility of whoever publishes the app.

Why it affects you even if you don't use Lovable

This is not a Lovable-only problem. Any app that talks to Supabase directly from the browser —whether you built it with Bolt, v0, Cursor, Replit or by hand— relies on RLS being correct on every table. The app looks perfect, login works, but real security lives in the database, not in the interface. Hiding a button in the frontend protects nothing: the REST request is still there for anyone who knows where to look.

The same goes for Firebase with rules in "test mode", for PocketBase without collection rules, or for any backend-as-a-service the browser calls directly. The pattern is always the same: the AI that generated your app knows how to make it work, but rarely writes the rules that stop it from working for the people it shouldn't.

How to know if your app is vulnerable (5 minutes)

Test 1: the incognito request

  • Open DevTools (F12) → Network tab while using your app and watch the calls to *.supabase.co/rest/v1/. Copy one of those URLs in full (the apikey travels in a header; in the URL you will see the table and the filter).
  • Open it in an incognito window, with no session. If it returns data, every visitor gets it too.
  • Try to over-ask: if a call returns your data, remove the filter from the URL (the ?user_id=eq.… part). If it hands back other users' rows, your RLS is not effective.

Test 2: the Supabase dashboard

Open your project → Table Editor. Every table without RLS carries a red "RLS disabled" badge. Then go to Authentication → Policies: a table with RLS enabled but no policy denies everything (good for the data, but your app will stop working), and one with a using (true) policy is as open as if RLS were off. The dashboard also has a Security Advisor (under Advisors) that lists exactly these tables.

Test 3: one SQL query

In the dashboard's SQL Editor, this query lists the tables in the public schema (the one the API exposes) that do not have RLS enabled:

select 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 = 'r'
  and not c.relrowsecurity;

If it returns any row holding real data, that table is open through the API right now.

How to fix it, step by step

1. Enable RLS on every table

Not just the "important" ones. The rule is deny by default: with no policy allowing it, nobody gets in. For every table in the public schema:

alter table public.customers enable row level security;

Once you do, your app stops seeing data until you write policies. That is expected: the door is now shut and you are about to open only what is needed.

2. Write explicit policies per operation

The most common pattern is "each user sees and edits only their own rows", comparing auth.uid() with the column that stores the owner. For a customers table with a user_id column:

create policy "customers: read own"
  on public.customers for select
  to authenticated
  using ((select auth.uid()) = user_id);

create policy "customers: insert own"
  on public.customers for insert
  to authenticated
  with check ((select auth.uid()) = user_id);

create policy "customers: update own"
  on public.customers for update
  to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "customers: delete own"
  on public.customers for delete
  to authenticated
  using ((select auth.uid()) = user_id);

Three details make the difference: to authenticated leaves anonymous visitors out; with check stops anyone from inserting or editing rows on someone else's behalf (without it, a user could create data with another user's user_id); and wrapping auth.uid() in a select makes Postgres evaluate it once per query instead of once per row, which on large tables multiplies performance. Add an index on user_id if you do not have one.

3. Never use the service_role key in the frontend

It bypasses RLS entirely: with it, every policy you just wrote does not exist. That key lives on the server only (Edge Functions, your own backend, a worker). If it was ever in client code, treat it as leaked and rotate it from the dashboard. Supabase now also offers newer keys (sb_publishable_… for the browser and sb_secret_… for the server) that make the split more obvious.

4. Re-test

Repeat the incognito test: the session-less request must return an empty list or a 401 error. Then log in with two different accounts and confirm neither sees the other's data. If you have automated tests, this is the case that deserves one.

Common mistakes while fixing it

  • Policies with using (true). AI assistants often suggest them "so it works". They leave the table as open as having no RLS at all.
  • Forgetting with check. It protects reads but allows writes on behalf of someone else.
  • "Catalog" tables with personal data. A public profiles table meant to show names ends up exposing emails or phone numbers. Expose only the needed columns through a view.
  • Views that bypass RLS. In Postgres a view runs with the permissions of whoever created it. Created from the dashboard, it may return rows the base table's policies would forbid. Create views with security_invoker = true (Postgres 15 or later) or review them one by one.
  • Storage without policies. Buckets have their own rules. A public bucket serves any file to anyone holding the URL; private documents belong in private buckets with per-user policies.
  • Fixing only production. If you keep a development project with copied data, it is just as exposed.

If you have already exposed data

First close the door (the steps above). Then rotate every key that sat in readable tables: third-party API keys, tokens, webhooks. Review the API logs in the Supabase dashboard to estimate what was read and since when. If the data includes personal information of European users, the GDPR requires you to assess whether the data protection authority must be notified within 72 hours and, in high-risk cases, the users themselves. Document what you found and what you did: you will be asked for it if anyone comes knocking.

A prompt to fix it with your AI

If you work with Cursor, Claude or Lovable's own chat, this prompt usually gets a good result: "List every table in the public schema of my Supabase project. For each one, enable Row-Level Security and write separate policies for select, insert, update and delete restricted to the authenticated role, so that each user only accesses rows whose user_id column equals (select auth.uid()). Use with check on insert and update. Tell me which tables have no owner column so I can decide those by hand. Never use using (true)."

How Scanaris checks it

Scanaris scans your site read-only and detects the fingerprint of this pattern: Supabase endpoints reachable from the browser, permissive CORS, keys and tokens exposed in your JavaScript, and data the app serves with no session. If you provide a read-only connection string, it also audits table by table whether RLS is enabled and whether policies exist. Paste your URL and it tells you, in seconds and ranked by severity, what is in plain sight —before someone else finds it.

Frequently asked questions

Has Lovable already fixed it?

Lovable changed how it generates tables and added warnings, but an app published before April 15, 2025 stays the way you left it: policies do not add themselves. And in any project, new or old, every table you create afterwards needs its own.

Does enabling RLS break my app?

Enabling it without policies does: everything is denied. That is the correct behaviour. Write the policies from step 2 and the app works again, but only for the people it should.

Can I protect the table from the frontend?

No. The frontend is code running on the visitor's computer and can be modified at will. The only thing that cannot be bypassed is the rules enforced by the database.

What if my app has no users, just a public content table?

Then a public read policy is legitimate: for select to anon, authenticated using (true). What must not exist is an open write policy or columns with personal data in that table.

How does your site do?

Run Scanaris and check it in 30 seconds, free.

Scan my site

Related checks

Security guides for your stack