SUPABASE RLS IN THE WILD: 2026 MISCONFIGURATION ATLAS
Supabase Row Level Security misconfiguration is the most-reproduced authorization failure in AI-built apps. This atlas breaks it down into five distinct failure modes — RLS off, permissive policy, partial coverage, service-role exposure, and auth.uid() misuse — each with a reproducible scenario on the gapbench public benchmark and a one-line fix.
Supabase is the default backend on Lovable and a common choice on Bolt.new, Cursor, and Replit — and Row Level Security is the only authorization layer between the public internet and the database, because the anon key ships to the browser by design. RLS misconfiguration is the failure mode we reproduce most consistently across engagements and on the gapbench public benchmark. This atlas breaks it into five mutually exclusive modes and tells you, per failure mode, what to look for and how to fix it.
If you build on Supabase, the catalog below tells you which failure mode is most likely on your stack and how to detect each one in seconds.
Atlas scope
| Field | Value |
|---|---|
| Window | Nov 2025 – Apr 2026 |
| Source | Anonymized customer engagements + gapbench reproducible scenarios |
| Calibration control | ref-rls — RLS done right on every verb |
| Public reproducibility anchor | supabase-clone — all four primary failure modes co-located |
| Verifying tool | Free Supabase RLS Checker — query any URL |
We do not publish a corpus-wide count of Supabase-backed apps or a single percentage rate, because the underlying engagements are anonymized and not a uniform random sample. The reproducibility anchor — the part anyone can verify in seconds with curl — is the gapbench scenario set.
Failure modes
We classify every RLS finding into one of five modes. The modes are mutually exclusive at the table level — one table is in exactly one mode.
| Mode | What it looks like in Supabase | Relative frequency |
|---|---|---|
| RLS off | “RLS Disabled” badge on the table | Most common |
| Permissive policy | RLS enabled, policy using (true) or similar |
Highly common |
| Partial coverage | SELECT locked, INSERT/UPDATE/DELETE open | Common |
| Service-role exposure | Service-role key reachable from client bundle | Catastrophic when present |
Misuse of auth.uid() |
Policy compares against wrong column or null-coalesces unsafely | Less common, hardest to detect |
The first two modes are the bulk of what we see. Both are failure modes that “RLS turned on” does not protect against — the Supabase dashboard shows a green checkbox in both cases.
Most-exposed table names
These are the table names most frequently found in a misconfigured state in engagements. The naming gravity of AI generators is real — different builders converge on the same names, which makes these the highest-yield enumeration targets for an attacker. Ranking reflects relative frequency, not absolute counts.
| Rank | Table name | Why it ranks here |
|---|---|---|
| 1 | users |
App-managed mirror of Supabase Auth; policies often forgotten |
| 2 | profiles |
Supabase Auth integration convention; policy lifecycle drift |
| 3 | invoices |
High-value owned resource; common across SaaS templates |
| 4 | messages |
Generated by chat/notification scaffolds |
| 5 | projects |
Multi-tenant SaaS scaffold default |
| 6 | subscriptions |
Billing scaffold; often added after initial RLS setup |
| 7 | comments |
Generated alongside any threaded-content feature |
| 8 | orders |
E-commerce scaffold; ownership policy often incomplete |
| 9 | tasks |
Task-manager scaffold default |
| 10 | notifications |
Generated alongside event/messaging features |
users and profiles rank highest because Supabase Auth integrates with a profiles table by convention, and AI generators mirror this with a separate users table for app-specific fields. When the policy on the auth-managed table is correct but the app-managed mirror is forgotten, both names show up exposed.
Per-platform modal failure mode
The dominant RLS failure mode we observe per platform, ranked by relative frequency in engagements.
| Platform | Relative RLS failure incidence | Modal failure mode |
|---|---|---|
| Lovable | Highest | Permissive policy |
| Bolt.new | High | RLS off |
| Replit | High | RLS off |
| Cursor | Moderate | Partial coverage |
| V0 (with Supabase backend) | Lower | Permissive policy |
Lovable’s elevated rate has a structural cause: the platform’s generator adds tables incrementally as features are added, and the policy-creation step does not always run on the new table. We see clean Lovable apps regress to a permissive state within a single feature addition. The longitudinal study tracks this pattern.
CWE / OWASP mapping by failure mode
Each failure mode maps to a different CWE because the type of authorization gap is different. Triage and fix differ accordingly.
| Failure mode | CWE | OWASP | Fix shape |
|---|---|---|---|
| RLS off | CWE-862 Missing Authorization | API1:2023 BOLA · A01:2021 | Enable RLS on the table; add a base policy |
Permissive policy using (true) |
CWE-863 Incorrect Authorization | API1:2023 BOLA · A01:2021 | Replace with using (auth.uid() = owner_id) |
| Partial coverage (SELECT locked, write open) | CWE-863 Incorrect Authorization | API1:2023 / API3:2023 BOPLA | Add INSERT, UPDATE, DELETE policies |
| Service-role exposure | CWE-732 Incorrect Permission Assignment | A05:2021 Security Misconfiguration · A02:2021 | Rotate key; route writes through Edge Function with anon-role context |
Misuse of auth.uid() |
CWE-863 Incorrect Authorization | API1:2023 BOLA | Compare against the correct column; never null-coalesce to a fallback |
The CWE-862 / CWE-863 split is the practical line. CWE-862 is “no check exists at all” — easy to find, easy to fix. CWE-863 is “a check exists but is wrong” — harder to find because the policy looks like a policy, and the dashboard reports green.
Per-mode fix patterns
The fix per mode is short and mostly mechanical. The trap is mode-confusion — applying the RLS-off fix to a permissive-policy table leaves the table unchanged.
-- Mode 1: RLS off. Enable RLS, then add a base policy.
alter table invoices enable row level security;
create policy "owner_select" on invoices
for select using (auth.uid() = owner_id);
-- Mode 2: Permissive policy. Drop the bad one, add the scoped one.
drop policy "anon_read_all" on invoices; -- was: using (true)
create policy "owner_select" on invoices
for select using (auth.uid() = owner_id);
-- Mode 3: Partial coverage. SELECT locked, but writes are open.
-- The policies for the other verbs are missing — add them.
create policy "owner_insert" on invoices
for insert with check (auth.uid() = owner_id);
create policy "owner_update" on invoices
for update using (auth.uid() = owner_id) with check (auth.uid() = owner_id);
create policy "owner_delete" on invoices
for delete using (auth.uid() = owner_id);
-- Mode 5: Misuse of auth.uid(). Wrong column or unsafe coalesce.
-- WRONG: null auth.uid() is silently treated as a match
create policy bad on invoices
for select using (coalesce(auth.uid(), invoices.owner_id) = invoices.owner_id);
-- RIGHT: compare against the correct column, return false on null
create policy good on invoices
for select using (auth.uid() is not null and auth.uid() = owner_id);
For mode 4 (service-role exposure), the fix is structural: rotate the key in the Supabase dashboard, audit the bundle to confirm the new key is not present, and route any operation that genuinely needs service-role through a server-only Edge Function with the key sourced from the function’s secrets vault — not from the build env.
Why “RLS turned on” is not enough
The mode that catches most teams off guard is “permissive policy” — the dashboard says “RLS Enabled” but the policy is using (true) and does nothing. A founder reading the Supabase docs and turning on RLS gets the green checkbox, sees the badge in the dashboard, and assumes the work is done. The policy using (true) is what AI generators most often produce when asked to “add an RLS policy” without a specific access rule, because it satisfies the surface requirement without restricting anything.
If you only check whether RLS is on, you will miss every failure of this mode.
Methodology
Source. Failure modes were identified across (a) anonymized customer engagements with Supabase-backed apps built on Lovable, Bolt.new, Cursor, Replit, and V0 between Nov 2025 and Apr 2026, and (b) deliberately vulnerable scenarios on the gapbench public benchmark, including supabase-clone (all four primary failure modes co-located), indie-saas, and multi-tenant-saas. We do not publish a corpus N because the engagement portion is anonymized by design and not a uniform random sample.
Probe. Each app’s anon key was extracted from the live JavaScript bundle. We enumerated tables via PostgREST OpenAPI introspection at /rest/v1/ and queried each table for unauthenticated reads. For tables that responded, we attempted unauthenticated writes (insert) to classify whether the failure was read-only or read-write.
Classification. Failure mode assignment is rule-based on the captured response code, response body, and PostgREST schema metadata. The full classification logic is reproducible by running the Supabase RLS checker against any URL.
Limits. This atlas covers what is reachable from outside with the anon key. It does not measure RLS misconfigurations behind the service-role key (which would require server-side access we do not have) or misconfigurations on tables that are present in the schema but not yet referenced by the OpenAPI spec.
Calibration against ref-rls. Every probe in the RLS suite is also run against ref-rls — a deliberately clean Supabase scenario where every table has the canonical using (auth.uid() = owner_id) policy on every verb. Any probe that fires on ref-rls is, by construction, a false positive — and the rule gets killed before it ships. Every recurrence claim in this atlas is net of false-positive elimination via the reference site.
Reproduce on the public benchmark
Each failure mode maps to a live scenario on gapbench.vibe-eval.com. The scenarios are running; the curl commands work today.
| Scenario | URL | Failure mode reproduced |
|---|---|---|
| Supabase clone | /site/supabase-clone/ | All four primary modes co-located on one app |
| Indie SaaS | /site/indie-saas/ | Permissive policy + service-role JWT in bundle |
| Multi-tenant SaaS | /site/multi-tenant-saas/ | Partial coverage — reads locked, writes open across tenants |
| ref-rls (clean control) | /site/ref-rls/ | The “RLS done right” exemplar; same shape, no leak |
For the anatomy of why the bug ends up shaped this way and the fix recipes per stack, see the companion pattern walkthrough: The Supabase service-role key in your frontend bundle.
How to reproduce a single data point
- Open the Supabase RLS Checker.
- Paste your live URL. The tool extracts your anon key from the bundle.
- The tool queries every public table and classifies the response.
A clean app reports zero exposed tables. An app with any non-zero count is in one of the five failure modes above.
Sources and references
- gapbench Supabase scenarios. supabase-clone, indie-saas, multi-tenant-saas, ref-rls (clean control).
- Supabase Row Level Security docs. supabase.com/docs/guides/database/postgres/row-level-security.
- PostgreSQL RLS reference. postgresql.org/docs/current/ddl-rowsecurity.html.
- OWASP API1:2023 BOLA and API3:2023 BOPLA. owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization.
- CWE-862 Missing Authorization, CWE-863 Incorrect Authorization, CWE-732 Incorrect Permission Assignment. cwe.mitre.org.
Citations
VibeEval. Supabase RLS in the Wild: 2026 Misconfiguration Atlas. May 2026. https://vibe-eval.com/data-studies/supabase-rls-misconfiguration-atlas-2026/
Related
- Pattern walkthrough: The Supabase service-role key in your frontend bundle
- Pattern walkthrough: BOLA in AI-generated CRUD — the missing ownership check
- Pattern walkthrough: False positives and the ref0 control — the calibration methodology behind every detection
- Data study: 2026 AI App Security Benchmark — full corpus
- Data study: Where Vibe Coders Leak Their Keys — 2026 Frontend Secrets Report
- Data study: Broken Object-Level Auth in AI-Generated CRUD
- Data study: Honeypot Supabase: How long does a public anon key survive? — the time-to-abuse measurement
- Guide: Is My Lovable App Secure? Builder Checklist
- Tool: Free Supabase RLS Checker
RUN IT YOURSELF
Each scenario below is live on the public benchmark. The commands are copy-paste ready. Outputs may evolve as we tune the scenarios; the bug stays.
curl -s 'https://gapbench.vibe-eval.com/site/supabase-clone/rest/v1/users?select=*' -H 'apikey: ANON_KEY' -H 'Authorization: Bearer ANON_KEY'
curl -s 'https://gapbench.vibe-eval.com/site/supabase-clone/rest/v1/invoices?select=*' -H 'apikey: ANON_KEY'
curl -s https://gapbench.vibe-eval.com/site/supabase-clone/ | grep -oE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' | head -1
curl -s 'https://gapbench.vibe-eval.com/site/ref-rls/rest/v1/invoices?select=*' -H 'apikey: ANON_KEY'
COMMON QUESTIONS
TEST YOUR SUPABASE RLS IN 10 SECONDS
Free RLS checker queries every public table from outside and reports which ones return rows without auth.