Back to the blog|AI Code Security

Supabase RLS: the one critical mistake that leaks AI-built apps

June 14, 2026
Timo WevelsiepTimo Wevelsiep
veriploy

Supabase RLS: the one critical mistake that leaks AI-built apps

Almost every AI-app data leak traces back to the same Supabase RLS mistake: missing Row Level Security. Here is how to find and fix it in minutes.

veriploy.de Blog

This post is for general technical information and does not replace an individual review or legal advice. As of the time of publication.

Almost every spectacular data leak in an AI-built app traces back to the same one mistake: Row Level Security is not enabled, while the public anon key sits in the browser. The key is public by design. RLS is the actual protection layer, and that is exactly what AI generators forget, app after app.

Table of Contents

The one mistake that explains almost every AI-app leak

I have looked at enough AI-built Supabase apps over the past months to see a pattern that repeats almost to the point of boredom. The headlines sound like sophisticated attacks, but the technical core is almost always the same: a table sits in the public schema, the public anon key is in the browser bundle, and Row Level Security is not enabled. That is all it takes. Whoever has the project URL and the key, and both sit openly in the shipped JavaScript, reads and writes straight into the database.

This is not an exotic edge case. It is by far the most common critical misconfiguration I meet in AI-generated apps. And it lands exactly in the category OWASP ranks first in the Top 10:2021: Broken Access Control. Per OWASP, 94 percent of applications were tested for some form of broken access control, and the category moved up from 5th to 1st place. OWASP is clear about it: access control is only effective in trusted server-side code or a server-less API, not in the client.[8] That is precisely where RLS sits in Supabase, server-side in the database. Take it away and you remove the only layer standing between an anonymous visitor and your data.

In this post I show how RLS really works, why AI generators trip over this in particular, and I walk the pattern through three real incidents: CVE-2025-48757, the Moltbook incident and a Lovable EdTech case. Then I show how to find and fix the mistake in your own app in minutes, and why a green linter is still not security.

How RLS in Supabase really works (and what the anon key is)

Supabase exposes a Postgres database and puts an auto-generated REST API in front of it. So the client can access it, there are two key types, and the difference is the whole point.

The publishable or anon key is, per Supabase, explicitly "safe to expose online", specifically on a web page, in a mobile or desktop app, in GitHub Actions, CLIs and in source code.[5] So it is not a secret but public by design. Open a Supabase app in the browser, look at the network tab, and you will find it. That is intended. The protection of your data comes, per Supabase, not from keeping this key secret but from Postgres and the built-in anon and authenticated roles.[5] And that is exactly where Row Level Security applies: RLS is Postgres' mechanism that decides per row which role may see or change what.

The second key is the secret or service_role key. Per Supabase it has full access to the project's data and bypasses RLS entirely. Supabase is unambiguous here: never expose it publicly, never use it in a browser, not even on localhost.[5] Ship that key client-side and you have a different, equally catastrophic problem. In the standard case, though, the issue is not the service_role key at all but the perfectly correctly exposed anon key meeting an unprotected table.

The decisive sentence is right in the Supabase docs: RLS must always be enabled on any table stored in an exposed schema, and by default that is the public schema.[4] When RLS is missing, per Supabase, any granted table without RLS is reachable by roles with matching Data API grants, for example anon.[6] In plain terms: without RLS the table is open to anyone with the project URL.

Why AI generators trip over this in particular

The trap lies in the default behaviour, and it is subtler than it sounds. Per the Supabase docs, RLS is enabled by default when a table is created via the Table Editor in the dashboard. But if the table is created via raw SQL or the SQL editor, you have to enable RLS yourself and grant each Postgres role only the permissions it needs.[4]

And that is exactly how AI generators work. They do not click in the dashboard, they write migrations and SQL. A create table statement from a generator therefore does not have RLS on automatically. If the generator does not also emit enable row level security plus matching policies in the same breath, you get exactly the open table we are talking about. The code "works" immediately: the app reads and writes, the feature is there, the demo flow runs. That the same table is also open to any stranger is not something the working happy path shows you.

On top of that comes the architecture pattern tools like Lovable use. Per Matt Palmer's CVE disclosure, Lovable relies exclusively on RLS for confidentiality and integrity while the client makes direct REST calls with the public anon key.[2][3] That is a legitimate pattern as long as RLS is configured cleanly. But it has no safety net whatsoever when RLS is missing. Palmer puts the lesson plainly: the core issue was not the exposed public key as initially assumed, because Supabase provides that by design, but the absent RLS configuration.[2]

That matches what I generally see with AI code, and which I describe in more detail in the launch checklist for production-ready software: generators build the feature, not the operational readiness. Authentication, the login, usually shows up. Authorisation, the question of who may see which specific row, is too often missing.

Three real incidents, one pattern: CVE-2025-48757, Moltbook, Lovable EdTech

Three cases, the same pattern. I deliberately write incident numbers as what they are: figures from the respective source, not my own audit.

CVE-2025-48757 (disputed by the vendor). Per the NVD, an insufficient Row-Level Security policy in Lovable-generated sites through 2025-04-15 allows remote unauthenticated attackers to read or write to arbitrary database tables of generated sites.[1] The rating is CVSS 9.3 (CVSS v3.1), CWE-863 (Incorrect Authorization), published 2025-05-29.[1] Important framing: the CVE is disputed by the vendor, on the grounds that each customer is responsible for protecting their own app's data.[1] Per Matt Palmer's disclosure, a scan found the pattern across 303 vulnerable endpoints in 170 projects, about 10.3 percent of 1,645 analysed Lovable apps. Palmer himself notes the scan only examined homepages, not login-protected areas, so the number is a lower bound.[2] The scope is confirmed secondarily by a further analysis that also cites over 170 affected apps.[11]

Moltbook (per Wiz Research). At the AI agent network Moltbook, per Wiz Research, a hardcoded Supabase publishable key sat in the client-side JS bundle, and per Wiz, without RLS policies this key grants full database access to anyone who has it. RLS was, per Wiz, fully disabled.[9] Per Wiz this exposed roughly 1.5 million API auth tokens, 35,000 email addresses and about 4.75 million records in total, discovered on 2026-01-31.[9] A second source rounds differently, citing about 1.49 million records, which is why I attribute the figures explicitly to Wiz.[12] Per Wiz it was fixed in several phases by 2026-02-01, simply by enabling RLS policies.[9] No zero-day, no sophisticated exploit. A missing enable row level security.

Lovable EdTech app (per a security report). Precision matters here, because two different incidents circulate. Per a security report (Bastion, disclosed 2026-02-27), an EdTech app exposed 18,697 user records, including 14,928 unique email addresses, split into 4,538 students, 10,505 enterprise users and 870 with full PII. Sixteen vulnerabilities were found, six of them critical. The app used the anon key for direct browser REST calls without RLS.[10] The most instructive finding: an inverted auth logic where the guard blocks the people it should allow and allows the people it should block.[10] Important: the platform-wide tenant-isolation incident of 2026-04-20 is a separate case that I do not conflate with this EdTech app.

The pattern across all three: not a clever attack, but an open or misconfigured door. That is exactly why this mistake is so dangerous. It is trivial to exploit and trivial to overlook.

The mistake behind the mistake: RLS on, but the policy wrong

It would be too easy if "enable RLS" solved the problem completely. It does not. Enabling RLS is the necessary condition, not the sufficient one.

The Lovable EdTech case shows it exemplarily: an inverted auth logic can let anonymous access through even with RLS enabled, because the policy checks exactly the wrong way around.[10] A policy that exposes too much is just as much a hole as a missing one. And an RPC function (a Postgres function exposed as an API) can return data that bypasses the RLS policy on the underlying table.

That is why you need two things: correct policies per access type, and a real multi-user test. The test is mundane, but it is the only one that genuinely answers the question: log in as user A, note a foreign record ID belonging to user B, and try to read it via the API. If that succeeds, the policy is wrong, no matter how green the linter glows. Conceptually this is exactly the IDOR test OWASP describes under A01: a parameter like an account ID is bent toward someone else's object.[8]

Check in ten minutes: find the mistake in your app

For the first check you need neither an audit budget nor special tooling. Three steps are enough for a solid first read.

Step 1: database linter. Supabase has a built-in linter. The relevant lint is rls_disabled_in_public with severity ERROR. It flags every public table without RLS. The lint's rationale is exactly our topic: if RLS is not enabled on a public table, anyone with the project's URL can create, read, update or delete (CRUD) rows in the impacted table.[7] Every ERROR from this lint is an open door.

Step 2: the real anon-key test. Take your app's public anon key, which you will find in the browser bundle anyway, and talk to the REST API directly with it. Try to read a table without logging in. If data comes back that only logged-in users should see, you have reproduced the mistake.

Step 3: the multi-user test. As above: user A tries to read user B's data via B's ID. This uncovers wrong policies that the linter does not see.

If even this short run gives you a bad feeling, that is a good signal to look closer. For a quick, structured first self-assessment with no setup I built the interactive AI app risk check: a few targeted questions, and at the end a rough read of where your app stands.

Fix it now: enable RLS and set correct policies

Enabling itself is one line. Per the Supabase docs:

alter table "table_name" enable row level security;

After that nothing is accessible via the anon key for the moment, and that is intended: without a policy, RLS exposes nothing by default.[4] Now you add exactly the policy you need, per access type. The common pattern from the docs, restricting each user to their own rows, looks like this for a profiles table:[4]

create policy "User can see their own profile only."
on profiles for select
using ( (select auth.uid()) = user_id );

Two things matter here. First: you need a matching policy per operation, so separately for select, insert, update and delete, depending on what should be allowed client-side. A select policy protects no writes. Second, the (select auth.uid()) instead of bare auth.uid(): this is a performance pattern from the docs, not a security pattern. Per Supabase, wrapping causes an initPlan so the Postgres optimiser evaluates the function once per statement instead of once per row.[4] In the docs benchmarks a wrapped uid policy drops from 179 ms to 9 ms, an improvement of about 95 percent.[4] On a large table that is the difference between usable and unusable. It does not make the policy safer, only faster.

Why a green Security Advisor is still not security

A green linter feels good but is no proof of security. It answers exactly one question: is RLS enabled on the public tables? It does not answer the more important ones.

It does not check whether your policies are functionally correct. It does not notice when the auth logic is inverted, as in the EdTech case.[10] It does not see whether a service_role key has somehow ended up in the client, which bypasses RLS anyway.[5] And it does not check whether an RPC function returns too much past the policy. A green linter means "no obvious configuration mistake". It does not mean "the access logic is correct".

That is not a failure of the tool but a limit of how it is built. A linter checks configuration, not functional behaviour. The question of whether user A may see user B's data is a business-logic question, and an automated check cannot answer it. That takes the multi-user test and a pair of eyes that understands what the app is actually supposed to allow.

My approach: ongoing oversight instead of a snapshot

I could sell you a one-off RLS check here, and for an initial inventory that makes sense. But I consider the one-off audit idea the wrong shape for AI-built apps. An RLS check is a snapshot. AI apps, however, change weekly: a new prompt, a new feature, a new migration, and there is another table without RLS in the public schema. The very speed that makes AI development attractive makes the one-off report stale after a short while.

That is why I work with Veriploy as ongoing technical oversight rather than a point-in-time audit: a regular look at repo, RLS and configuration risks, CVEs and infrastructure, prioritised risks and clear next steps instead of a wall of scanner output. If you want to start with an inventory, you begin with the Baseline (790 EUR one-off). If you want ongoing support, the plans are tiered: from a monthly look (Oversight, 990 EUR per month) through closer sparring (Guard, 1,950 EUR per month) to close support for live products (Launch, 3,900 EUR per month), Scale on request.

Clearly scoped, because this matters to me: Veriploy does not make your app secure, gives no guarantee and is no penetration-test replacement. It is also not feature development. It creates visibility and prioritisation so you can decide, with a clear basis, what really needs to be closed first. With a mistake as trivial to exploit and as easy to overlook as missing RLS, that second pair of eyes is often the difference between a quiet night and a headline.

Sources

  1. NVD, CVE-2025-48757 (CVSS 9.3, CVSS v3.1, CWE-863, disputed by vendor, published 2025-05-29): https://nvd.nist.gov/vuln/detail/CVE-2025-48757
  2. Matt Palmer, "Statement on CVE-2025-48757" (disclosure, 303 endpoints / 170 projects / 1,645 scanned, homepages only): https://mattpalmer.io/posts/statement-on-CVE-2025-48757/
  3. Matt Palmer, "CVE-2025-48757" (technical analysis of the RLS pattern): https://mattpalmer.io/posts/CVE-2025-48757/
  4. Supabase Docs, Row Level Security (default behaviour, policy syntax, initPlan performance, benchmarks): https://supabase.com/docs/guides/database/postgres/row-level-security
  5. Supabase Docs, API Keys (publishable vs. secret key, RLS protection, "never use in a browser"): https://supabase.com/docs/guides/api/api-keys
  6. Supabase Docs, Securing your API (a table without RLS is accessible to anon): https://supabase.com/docs/guides/api/securing-your-api
  7. Supabase Splinter, Lint 0013 rls_disabled_in_public (severity ERROR, remediation): https://supabase.github.io/splinter/0013_rls_disabled_in_public/
  8. OWASP Top 10:2021, A01 Broken Access Control (#1 risk, 94%, server-side enforcement, IDOR): https://owasp.org/Top10/2021/A01_2021-Broken_Access_Control/
  9. Wiz Research, "Exposed Moltbook database reveals millions of API keys" (per Wiz: 1.5M tokens, 35,000 emails, ~4.75M records, RLS disabled, discovered 2026-01-31): https://www.wiz.io/blog/exposed-moltbook-database-reveals-millions-of-api-keys
  10. Bastion, "Lovable Data Breach" (EdTech app disclosed 2026-02-27: 18,697 records, 14,928 emails, 16 vulnerabilities, inverted auth logic): https://bastion.tech/blog/lovable-april-2026-data-breach/
  11. Superblocks, "Lovable vulnerability explained: how 170+ apps were exposed" (secondary confirmation of scan scope): https://www.superblocks.com/blog/lovable-vulnerabilities
  12. Implicator.ai, "Moltbook left every AI agent's API keys in an open database" (secondary confirmation, rounds to ~1.49M records): https://www.implicator.ai/moltbook-left-every-ai-agents-api-keys-in-an-open-database-security-researcher-finds/

Frequently Asked Questions

What is the most common critical Supabase RLS mistake in AI-built apps?
Row Level Security is not enabled on a table exposed in the public schema, while the public anon key sits in the browser bundle. Supabase is explicit here: RLS must always be enabled on tables in an exposed schema, by default the public schema. Without RLS, Supabase says any role with a matching Data API grant, such as anon, can access the table, meaning anyone with the project URL.
Is Supabase's public anon key a security problem?
No. The publishable or anon key is, per Supabase, 'safe to expose online', for example on a web page or in a mobile app, and public by design. Per Supabase, the actual protection comes via Postgres and the anon and authenticated roles, that is, via RLS, not via keeping the key secret. The mistake is not the exposed key but the missing RLS configuration. The secret or service_role key bypasses RLS and, per Supabase, must never be used in a browser, not even on localhost.
What was CVE-2025-48757?
A vulnerability published in 2025 and disputed by the vendor (CVSS 9.3, CVSS v3.1, CWE-863, published 2025-05-29). Per the NVD, an insufficient RLS policy in Lovable-generated sites through 2025-04-15 allows remote unauthenticated attackers to read or write to arbitrary database tables. Per Matt Palmer's disclosure, a scan found the pattern across 303 endpoints in 170 projects, about 10.3 percent of 1,645 apps examined. The vendor disputes the CVE, arguing each customer is responsible for protecting their own app's data.
What happened in the Moltbook incident?
Per Wiz Research, the AI agent network Moltbook had a hardcoded Supabase publishable key in its client-side JS bundle, and RLS was fully disabled. Per Wiz, this exposed roughly 1.5 million API auth tokens, 35,000 email addresses and about 4.75 million records in total, discovered on 2026-01-31. A second source rounds differently (about 1.49 million records). Per Wiz it was fixed in several phases by 2026-02-01 by enabling RLS policies, no sophisticated attack required.
Is enabling RLS enough to make my data safe?
Enabling RLS is mandatory but not sufficient. An enabled but wrong policy can expose too much or, as in the Lovable EdTech case, invert the auth logic so the guard blocks the people it should allow and allows the people it should block. You need correct policies per access type plus a real multi-user test: user A must not see user B's data.
How do I check whether RLS is enabled on all my tables?
Supabase has a database linter that flags the lint rls_disabled_in_public (severity ERROR): every public table without RLS. Per that lint, without RLS anyone with the project's URL can create, read, update or delete rows. Add a practical test using the public anon key against the REST API and an attempt to read data with someone else's ID. Both belong in every pre-launch check.
How do I enable RLS and set a correct policy?
Enable with alter table "table_name" enable row level security; Then one policy per access type, per the Supabase docs for example for own rows create policy ... on profiles for select using ( (select auth.uid()) = user_id );. Important: after enabling, nothing is accessible via the anon key until you add a policy, and that is intended.
Why should I wrap auth.uid() as (select auth.uid())?
It is a performance pattern from the Supabase docs: per the docs, wrapping causes an initPlan so the Postgres optimiser evaluates the function once per statement instead of once per row. In the docs benchmarks a wrapped policy query drops, for example, from 179 ms to 9 ms. It does not make the policy safer, just usably fast on large tables.
Is a green Supabase Security Advisor proof of security?
No. The linter flags missing RLS but does not check whether your policies are functionally correct, whether the auth logic is inverted, whether a service_role key sits in the client, or whether an RPC function returns too much. A green linter means 'no obvious configuration mistake', not 'the access logic is correct'.
Does Veriploy make my Supabase app secure, or is it a pentest?
Neither. Veriploy gives ongoing technical oversight: visibility into repo, RLS and configuration risks, CVEs and infrastructure, prioritised risks and clear next steps. It is not a penetration test, not a security guarantee and not feature development, but a second pair of eyes with senior experience.
Timo Wevelsiep

Written by

Timo Wevelsiep

Developer & Founder · Veriploy

Veriploy: technical oversight for AI-built software. Run by Timo Wevelsiep.

Repo fit

Check repo fit

Briefly describe the project.

Direct contact with me, no anonymous ticket system. I get back to you with a first assessment and the right entry point.

Timo Wevelsiep

Timo Wevelsiep

Software engineer, cloud architect, founder & managing director

[email protected]

By submitting, you agree to our Privacy Policy.

or