Menu

Security research3 min read

Row-level security mistakes in AI-built apps

When the browser talks to the database directly, row-level security is the only thing standing between a visitor and every row. A policy that exists is not the same as one that works.

Cover: 10.3% of 1,645 scanned Lovable apps exposed data through weak row-level security

Key takeaways

  • In client-to-database architectures, row-level security is the authorization layer. There is no server in between to catch mistakes.
  • A scan of 1,645 Lovable apps found 303 vulnerable endpoints across 170 projects (CVE-2025-48757).
  • A policy of using (true) turns RLS on and still lets anyone read every row.
  • Test policies by querying as an anonymous visitor and as a second user, not by checking that a policy exists.

Why RLS carries so much weight in AI-built apps

Many prompt-to-app platforms generate a frontend that talks straight to a hosted Postgres database such as Supabase. The browser holds a public key, the anon key, and sends queries directly. That is fast to build and removes a whole server tier.

It also means the database has to decide, row by row, who may read or write what. That decision lives in row-level security policies. If a policy is missing or too loose, the public key in every visitor's browser becomes a key to the table.

What the 1,645-app scan found

In March 2025, Matt Palmer and Kody Low wrote a script that visited the homepage of each of 1,645 apps on Lovable Launched, captured the database requests each page made, and tried widening them to select every row. They found 303 endpoints across 170 projects, roughly 10.3% of the sample, with inadequate RLS. Their disclosure lists names, phone numbers, subscriptions, payment details, and API keys among the exposed data. The issue became CVE-2025-48757.

The scan only touched homepages and never signed in, so the real number is higher. In a later check on one site, RLS looked active for signed-in requests, but removing the authorization header let them read the table and insert a record marked as paid.

Palmer's statement also notes that the platform's security scanner at the time checked whether any RLS policy existed, not whether it was correct. That gap is the lesson for everyone, not only Lovable users.

A policy that exists is not a policy that works

The most common failure is a policy that is technically present and functionally open:

Looks protected, is not
alter table orders enable row level security;

-- RLS is on, and every visitor can still read every order
create policy "read orders" on orders
  for select using (true);

A safer policy ties each row to the signed-in user:

Scoped to the owner
create policy "read own orders" on orders
  for select using (auth.uid() = user_id);

Other patterns to look for: insert or update policies without a with check clause, tables added later with RLS never enabled, and views or functions that bypass the policies on the tables underneath.

How to test it

  1. List every table the client can reach and confirm RLS is enabled on each one.
  2. Read each policy and ask: whose rows does this return? true is never the right answer for private data.
  3. Query each table with only the anon key and no session. Private tables should return nothing.
  4. Sign in as a second user and request the first user's rows by ID. You should get nothing back.
  5. Repeat for insert, update, and delete, not just select.

Only test projects you own or are authorised to test.

Where Routelint fits

Routelint does not read your database policies. It records the API calls your app actually makes, including direct calls to a hosted database, and replays them with no session and as a peer account. If a policy is open, the replay returns data it should not, and that response becomes the finding with redacted evidence.

For the general pattern, see what an IDOR is and the cross-user example.

Questions

Does enabling RLS make a Supabase table secure?
No. Enabling RLS with no policies blocks access, but a permissive policy such as using (true) re-opens the table to everyone. Each policy has to scope rows to the right user.
Is the Supabase anon key a secret?
No. It is meant to be public and ships in the browser. That is exactly why row-level security, not the key, has to enforce who can read each row.

Routelint records the API calls your running app makes, then replays them with no session and as a second account. One scan needs no account.

Sources

  1. Statement on CVE-2025-48757 — Matt Palmer
  2. API1:2023 Broken Object Level Authorization — OWASP API Security Top 10

Published November 1, 2026 by the Routelint team.

Security research2 min read

Exposed secrets and personal data in vibe-coded apps

A passive scan of 5,600 public apps turned up hundreds of live secrets and real personal data. Most of it was sitting in places any visitor's browser already downloads.

Security research2 min read

Why static analysis misses access-control bugs

A static scanner can tell you a query is unsafe. It cannot tell you that User B should not be able to read invoice 42. That rule lives in your business, not your syntax.