TL;DR

On 2026-09-25 UpGuard published the largest measurement to date of the failure mode this repo has tracked as ongoing-vibe-platform-exposure: of roughly 300,000 domains fingerprinted as Supabase-backed, 16,326 databases returned readable table contents to an unauthenticated query using nothing but the public anon key and project URL every front end ships. More than half of those show personal-data indicators; a smaller share exposes passwords and authentication tokens, and a very small number expose plausible payment-card data. The case studies are not toys: a US valet service with 100,000+ customer records and 78,000 licence plates, a Philippine OTP relay with 100,000+ SMS messages (passcodes included), a consulate's 25,000 applicant records with emergency-housing addresses, and a Canadian immigration coaching service storing 884 plaintext passwords. TechCrunch carried Supabase's response the same day. If you built on Supabase with an AI tool, assume any table created by a migration or through the API has RLS off unless you turned it on yourself — check today.

What happened

UpGuard (Greg Pollock, Director of Research) built a corpus of Supabase-backed sites from two sources — BuiltWith's technographic index and the raw JavaScript bundles in the Chrome UX Report dataset on BigQuery — by matching the Supabase project-URL and key patterns that every client bundle embeds. Roughly 300,000 domains matched. For each, UpGuard issued the request a browser would make for a table named users; the response classifies the project as readable (rows returned), readable but differently named (the API lists other accessible tables), or protected. 16,326 projects returned readable rows. UpGuard then sampled contents for PII indicators (names, emails, phones, addresses), credential indicators (password and token columns) and card-number patterns, and notified the owners of the most significant exposures.

The finding is a distribution, not a bug: Supabase has had RLS on by default for tables created in the Table Editor since March 2025, but tables created programmatically — by a migration, by CREATE TABLE in the SQL editor, or by an AI coding tool writing SQL for you — still default to RLS off, and a table with RLS off is world-readable through PostgREST to anyone holding the anon key, which is by design public. UpGuard's framing: "data leaks are the multiplicative product of a technology's ease of misconfiguration and the size of its user base," and it explicitly names AI coding tools writing migrations as the reason the second factor has grown — the report calls Supabase "the most recommended database product by Claude Code."

Geographically, European projects were less likely to leak (UpGuard credits data-protection law), and the exposed set skews towards ecommerce and restaurant apps (payment data) and unlicensed betting sites (passwords). UpGuard's report also collects the smaller prior measurements — Matt Turner's Lovable finding (CVE-2025-48757, March 2025), Modern Pentest's 28 % of 107 YC startups, Symbiotic's 39 of 1,072 apps, Escape's 175 databases in ~1,400 apps, Red Access's ~5,000 accessible apps in 380,000 URLs, and Wiz's Moltbook 1.5 M-token leak — so this is the sixth independent measurement of the same mechanism in eighteen months, at ten to a hundred times the sample size.

Supabase's statement to TechCrunch (CISO Bil Harmer): projects are "secure by default," security is "a shared responsibility between the company and its customers," and "Security at Supabase is never finished. We care deeply about getting it right, and we'll keep making it easier for every developer to ship securely." Neither the UpGuard report nor TechCrunch records a change to the programmatic-table default.

Am I affected?

You are in the exposed population if any of these is true:

  1. Your app's client bundle contains a Supabase project URL and anon key (it does — that is how Supabase works) and at least one table was created outside the Table Editor without an explicit ALTER TABLE … ENABLE ROW LEVEL SECURITY.
  2. An AI tool (Lovable, Bolt, v0, Replit, Claude Code, Cursor, Copilot) wrote your schema or migrations. UpGuard's report and this repo's own Lovable/Supabase findings both show generated migrations routinely omit RLS.
  3. You enabled RLS but wrote no policies, or wrote a permissive USING (true) policy to "make it work."

Check every table in one query from the SQL editor (the Supabase dashboard's Security Advisor lints the same thing):

select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public' and rowsecurity = false;

Any row returned is a table PostgREST will serve to the anon role subject only to the role's grants — and the default grants on public are broad. Then confirm from the outside, with your own project's public values (this is your own system, not someone else's):

curl -s "https://<your-project>.supabase.co/rest/v1/users?select=*&limit=1" \
  -H "apikey: <your anon key>" -H "Authorization: Bearer <your anon key>"

A JSON array with a row in it is the exposure UpGuard measured. Also grep your repo for the service_role key — it bypasses RLS entirely and must never reach a client bundle or a committed .env.

If you are affected

  1. Enable RLS on every public table now (alter table public.<t> enable row level security;). With no policies this blocks all anon access immediately; the app may break, which is the correct failure direction. Then add scoped policies (auth.uid() = user_id), not USING (true).
  2. Assume the data was read. UpGuard's scan was one of many; the anon key has been in your bundle since launch. If passwords were stored in your own tables (rather than Supabase Auth), force resets; if tokens, revoke them. → playbooks/if-your-webapp-was-compromised.md for the disclosure and triage order.
  3. Rotate any service_role key that was ever in client code or git history → playbooks/rotating-cloud-credentials.md.
  4. Run the full checklist in → playbooks/auditing-a-vibe-coded-repo.md; RLS is the first item because it is the one that leaks whole tables.

Prevention

  • Treat "RLS on" as part of the migration, not a dashboard setting. Every create table in a migration gets enable row level security and at least one policy in the same file, so a regenerated schema cannot silently regress. Put it in the prompt for whichever AI tool writes your SQL ("every table must enable RLS with per-user policies") and in code review.
  • Lint for it in CI. A query like the one above, run against a preview branch and failing the build on any rowsecurity = false row, is a five-line check. → prevention/ci-cd-hardening.md.
  • Never store passwords in your own table — use Supabase Auth; 884 plaintext passwords in one case study existed only because the app rolled its own.
  • Keep service_role server-side only → prevention/credential-hygiene.md.
  • Related pattern file, updated with each measurement: ongoing-vibe-platform-exposure.md.

Update 2026-09-27 — a live case study with CVE numbers: Capgo's Supabase backend received 19 CVEs on 2026-09-26, and several are RLS policies that were on and still wrong

This file is about tables with RLS off. The day after it was written, VulnCheck published 19 CVEs against Capgo (capgo.app, an over-the-air update platform for Capacitor apps whose backend is Supabase — PostgREST, Edge Functions and a service_role worker), following five on 2026-09-10 (including CVE-2026-88864, Critical: the public.sso_providers table without write restriction). Capgo is a SaaS vendor's own backend, not a vibe-coded app, and is logged here rather than filed separately — but three of the entries are the precise failure this file's "enable RLS" advice does not by itself prevent, and every Supabase-backed app built by an assistant has the same three seams:

  • A legacy table left in public is served, policies and all. CVE-2026-100623 (CVSS 8.7): the old membership table public.org_users "is directly exposed via Supabase PostgREST," and its RLS policies let an organization admin insert or update membership rows directly, "bypassing the invitation and role-assignment workflow entirely" — the invitation flow the API enforced never existed at the table.
  • A row-level policy is not a column-level one. CVE-2026-100616 (CVSS 7.0): the UPDATE policy on public.orgs lets an admin with org.update_settings "update the entire row, including the internal billing pointer column customer_id"; the official endpoint restricted editable fields, but a direct PostgREST PATCH is not the endpoint, so an admin without org.update_billing can null or corrupt the Stripe pointer and move the org to unpaid/no-plan behaviour.
  • A service_role worker that trusts user-writable rows is an RLS bypass by construction. CVE-2026-100619 (CVSS 8.7): a RESTRICTIVE policy blocks direct inserts into public.manifest, but the async on_version_update worker "trusts the manifest record contents and uses a service-role Supabase client to insert attacker-controlled file_name, file_hash, and s3_path values into public.manifest" from a version row the attacker could edit — OTA manifest poisoning served to every client on the channel. "No patch was available at publication."

What to add to the checks above. After rowsecurity = false returns nothing, look at what the policies say: any UPDATE policy without a column restriction on a table that holds a billing, role or owner column; any table you stopped using but never dropped; and any Edge Function or worker that reads a user-writable table with the service_role key and writes on the strength of what it read. Column grants (revoke update (customer_id) on public.orgs from authenticated;) and views are the tools PostgREST honours; a field allowlist in your API layer is not.

-- UPDATE policies on tables that carry ownership/billing/role columns
select p.tablename, p.policyname, p.cmd, p.qual, p.with_check
from pg_policies p join information_schema.columns c
  on c.table_schema = p.schemaname and c.table_name = p.tablename
where p.schemaname = 'public' and p.cmd in ('UPDATE','ALL')
  and c.column_name ~ '(customer|owner|role|plan|billing|admin)';
-- Tables in public with no rows written in the last 90 days but still granted to anon/authenticated
select relname, n_tup_ins, n_tup_upd from pg_stat_user_tables where schemaname = 'public' order by n_tup_upd asc limit 20;

Update 2026-10-07 — the platform change that would have prevented most of this lands for existing projects on 2026-10-30, and by Supabase's own text it does not touch the tables already exposed

Supabase's changelog entry "Breaking Change: Tables not exposed to Data and GraphQL API automatically" (published 2026-04-28) sets the schedule: new projects could opt in from 04-28, the opt-in became the default for all new projects on 2026-05-30, and "on October 30, 2026 the setting will be applied to all existing projects." After rollout, "new tables you create in public schema require an explicit opt-in (via a Postgres grant) before the Data API can see them." The sentence that matters for the 16,326 databases above: "Existing tables are not affected in your project, they keep their current grants and stay reachable." The entry is also explicit that "RLS behavior remains unchanged. Grants are a separate layer: they control whether a role can access a table at all, while RLS controls which rows that role can see" — so the change closes the creation-time exposure that AI-generated migrations produce (a table appears, anon can select), and does nothing for a table created before the rollout. Previously the platform granted select/insert/update/delete on every public table to anon, authenticated and service_role at creation; a missing grant now returns PostgREST error 42501 with a hint naming the GRANT. The change applies only to apps using the Data API (supabase-js and the client libraries); direct Postgres connections are unaffected.

For readers of this file: after 10-30, a new table an agent creates is private until granted, which is the right default — but every table UpGuard could read in September stays readable until someone revokes the grant or enables RLS. The "Am I affected?" checks above are unchanged. Status stays ongoing.

Sources