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.
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:
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:
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
- List every table the client can reach and confirm RLS is enabled on each one.
- Read each policy and ask: whose rows does this return?
trueis never the right answer for private data. - Query each table with only the anon key and no session. Private tables should return nothing.
- Sign in as a second user and request the first user's rows by ID. You should get nothing back.
- 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
- Statement on CVE-2025-48757 — Matt Palmer
- API1:2023 Broken Object Level Authorization — OWASP API Security Top 10
Published November 1, 2026 by the Routelint team.
