Skip to content

Build1 publisher3 min readPublished

Lovable's security scanner clears an RLS policy that returns every customer's orders

A Row Level Security policy can pass Lovable's scanner, restrict itself to authenticated users, join a membership table, and still hand every order in the database to anyone who signs up. Two SQL queries find it.

The Engineer · Build desk

What happened

  • Matt Palmer reported in March 2025 that applications generated by Lovable were shipping without working Row Level Security, and the finding became CVE-2025-48757.
  • The exposed data included emails, phone numbers, payment and subscription records and third-party API keys, some of it with write access, so payment records could be modified and not only read.
  • Lovable shipped a built-in security scanner in version 2.0 that checks whether RLS is enabled on a table and does not check whether the policies on it do anything.
  • A dev.to post now lists eight failure modes, ordered roughly by how badly they end, each checkable from the Supabase SQL editor in a couple of minutes.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Anyone who opens devtools has the anon key from the frontend bundle, and every table in the public schema answers it over REST, so a table with RLS switched off is a published endpoint.
  • constraint According to the post, the tenant boundary is the one thing a prompt cannot convey, so policy work stays manual and table-by-table no matter how good the generated schema is.
  • decision Turning on anonymous sign-ins is a single Auth toggle that changes what every existing "to authenticated" policy permits. Whoever owns access control has to make that decision.

Read the membership policy out loud and it asks a question about the user, never about the row. The condition is `exists (select 1 from public.memberships m where m.user_id = auth.uid())`, and nothing in it refers to a column of `orders` [17]. Postgres runs that test for every candidate row, gets the same answer every time, and returns the row [17]. One membership in one organisation is enough. The route the post describes is sign up, create your own free workspace, and read every order of every customer [18].

"This is the one the scanners cannot see, because structurally these are real policies," the author of the dev.to post wrote [14]. That is fair about what an enabled-flag check can observe: RLS is on, a policy exists, it restricts to `authenticated`, and it queries a membership table [17]. The author is also explicit that the generators are not the target. "None of this is a criticism of the tools. They generate working code quickly," the post says [20].

The blunt version of the same bug is `using (true)`, which the post says turns up when someone hits a permission error during development and asks an AI to fix it [15]. It does fix the error. Every logged-in user then reads every order in the system [15].

The failure the author ranks worst is role data in `user_metadata`. That object is writable by the user, so any logged-in account can open a browser console and run `await supabase.auth.updateUser({ data: { role: 'admin' } })` [8]. "If your policies trust that field, you do not have access control, you have a suggestion," the author wrote [9]. The suggested fix is a membership table keyed by `auth.uid()`, readable by its owner and writable by nobody from the client; `app_metadata` is the other option because it is not user-writable, and the author prefers the table as easier to reason about and to audit later [11].

The checks are two queries. One selects `tablename, policyname, qual, with_check` from `pg_policies` where the policy body matches `%user_meta%` or `%raw_user_meta%`, and the post treats any row returned as a critical finding [10]. The other reads `relrowsecurity` from `pg_class` for ordinary tables in the `public` schema, where false means the table is served to the anon key [13][12].

CVE-2025-48757 counted 303 vulnerable endpoints across more than 170 applications [2], about 1.8 endpoints per application [19]. That is one scan of one generator's output at one moment. The post does not disclose how many tables those applications had. For the ratio to transfer you would need the same generator, the same era of defaults, and the same habit of leaving tables in the `public` schema.

One item on the list sits oddly with the rest of it. The author writes that every pattern shown leaves RLS enabled and shows green [5], while the second pattern is RLS being off on some tables, which the Supabase dashboard does warn about; the author's point there is that the warning is easy to dismiss when you are shipping [13].

What to watch

  • Whether Lovable's scanner starts evaluating what a policy condition filters instead of whether RLS is enabled.
  • Whether Supabase changes the defaults the post relies on: anonymous sign-ins as a single Auth toggle, and user_metadata being client-writable.
  • A second scan of the CVE-2025-48757 population, which would show whether the 303 endpoints were actually repaired.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories