24,664 AI-built apps sent database records to any visitor. I built a one-command check that proves yours does not.
In November 2026, German security firm Crissy Field (Hamburg), working with the newspaper Handelsblatt, published research on AI-built apps. The numbers are the reason I built this tool, so here they are first, with the caveats attached.
The researchers scanned 159,041 Lovable projects found in Common Crawl (apps created December 2025 to September 2026). Of those, 34,545 used Supabase as their backend. 24,664 websites sent database records to the browser as soon as the page loaded, without any login or interaction. With further read-only requests using the same public key the app itself ships, the researchers found records containing email addresses in 10,560 tables across 7,403 sites (30%). Worst cases, each a separate app: one service provider exposed more than 1,000 identity records (passport numbers, expiry dates, nationalities); one lifestyle forum exposed more than 200,000 private chat messages and 80,000 profiles; one app exposed about 280,000 customer emails.
Read those numbers honestly, because the researchers are explicit about the limits:
- 30% is NOT a confirmed leak rate for all Lovable apps. The sample is not representative, some email addresses are public contact or test data, and records without an email address were not counted.
- The researchers could not establish whether any exposed data was misused.
- Record counts are a floor, not a total: only part of each dataset was retrieved.
- This was not a hack of Lovable's systems. The researchers used read-only requests with the same public key the app itself ships. The root cause is Row Level Security left off in the app's Supabase database, with the public anon key doing the rest.
The same class of bug had already been disclosed eight months earlier as CVE-2025-48757 (March 2025, CVSS 9.3): researchers scanned 1,645 Lovable showcase sites and found 170 with tables open to anonymous reads. Lovable added a Security Scanner and an AI Security Reviewer after that, and the exposure repeated at a larger scale in November. The lesson is not "run a scanner once". It is "prove, on every deploy, that an anonymous request returns zero rows".
I built a small tool that does exactly that. It is called vibeleak, and it is a zero-dependency TypeScript CLI. MIT licensed. It starts from only the deployed app URL, finds the Supabase project URL and anon key in the JavaScript the app already ships, then probes each table and storage bucket as an anonymous visitor. One command, one verdict: CLEAN, LEAK, or REVIEW, with a JSON report and a baseline diff so a closed table that opens again fails CI.
The problem: the key is already public, and nobody checked the lock
When an AI tool builds an app on Supabase, it ships a public anon key in the frontend JavaScript. That is by design: the browser needs the key to talk to the database. The only thing standing between a stranger and your tables is Row Level Security. If RLS is off, or a policy says USING (true), the anon key reads the whole table, and the app helpfully sends the rows to any visitor on page load.
Nobody checks this because the app works. The login page looks right, the dashboard loads, the data is there for the right user. The failure is invisible from inside the app. You only see it when you make the same request the app makes, without logging in, and count the rows that come back.
That is what vibeleak does. It fetches the app the way any visitor does, extracts the Supabase URL and anon key from the shipped JavaScript (the same code every browser downloads), enumerates the tables through the PostgREST OpenAPI document, and probes each one with an anonymous GET limited to one row. It stores column names and a 0/1 rows-visible flag only. It never stores row values. It never writes anything: GET and HEAD only, no POST, no RPC calls, no writes of any kind.
What it does
Five commands:
vibeleak scan <app-url> [--i-own-this-app] [--out report.json] vibeleak probe --url <project-url> --anon-key <key> [--tables a b] vibeleak check [--report report.json] [--baseline baseline.json] vibeleak diff --baseline baseline.json [--report report.json] vibeleak init [--dir .] -
scan: fetch the app URL, collect same-origin JavaScript (max 10 files, 2 MB each), extract the Supabase URL and anon key, then probe. If aservice_roleJWT orsb_secret_key is found in the shipped bundle, that is a CRITICAL finding and probing stops: an admin key in the frontend makes table probing meaningless. -
probe: the same engine with an explicit project URL and anon key, for apps with no public frontend or when the key is already known. -
check: read a report, compare it with a baseline and thevibeleak.jsonallowlist. Exit 0 CLEAN / 1 LEAK / 2 input error / 3 REVIEW. A table that was clean in the baseline and now leaks is a regression and fails. -
diff: compare the current report with a baseline: new leaks, regressions (closed then re-opened), and fixed tables, one line each. -
init: write a samplevibeleak.json(project URL,publicTablesallowlist,failOn) and an empty baseline. It never writes a key into a file: keys come from a flag orVIBELEAK_ANON_KEYand are never logged.
Verdict rules, evaluated in order:
-
service_roleJWT orsb_secret_in shipped JavaScript - CRITICAL, probe stops. - Table outside the allowlist, anonymous GET returns 1 or more rows - HIGH, LEAK.
- Bucket outside the allowlist, anonymous object listing works - HIGH, LEAK.
- Allowlisted table exposes a sensitive-looking column name (
password,secret,ssn,passport,token,api_key, name match only, values never read) - MEDIUM, REVIEW. - Tables cannot be enumerated (OpenAPI disabled, no
--tables) - LOW, REVIEW. - Every non-allowlisted table returns 0 rows, no V1/V3 - CLEAN.
Only scan apps you own or are authorised to test in writing. scan and probe require --i-own-this-app (or VIBELEAK_I_OWN_THIS_APP=1). Without it the command exits 2 and sends no request. Only anon keys are accepted: a service_role or authenticated JWT is refused before any request, and an sb_secret_ key is never sent anywhere.
A typical CI flow:
vibeleak scan https://my-app.example.com --i-own-this-app --out report.json vibeleak check --report report.json --baseline vibeleak.baseline.json --fail-on high vibeleak diff --baseline vibeleak.baseline.json --report report.json The demo: a leaking fixture, then a fix, then proof
The GIF above is a real terminal session against synthetic local fixtures: a fake Lovable-style app and a fake PostgREST server on 127.0.0.1. No real app and no real Supabase project was probed. The anon key is a self-signed synthetic JWT, and the row values are fake.
The leaky fixture scans LEAK:
$ vibeleak scan <app-url> --i-own-this-app --out report-before.json scanning http://127.0.0.1:37965 ... project: http://127.0.0.1:37965 (Supabase URL found in shipped JavaScript) anon key: found (role anon) tables: 3 enumerated via OpenAPI orders: 0 row(s) visible profiles: 1 row(s) visible, columns: id, email, passport_no public_posts: 1 row(s) visible, columns: id, title, body (allowlisted: public by design) storage buckets: 3 uploads: object listing allowed public-assets: object listing allowed (allowlisted) private-files: not listable findings: LEAK: profiles (1 row visible, columns: id, email, passport_no) - anonymous GET returns 1 row(s); table is not allowlisted LEAK: uploads - anonymous object listing is allowed; bucket is not allowlisted verdict: LEAK - an anonymous visitor can read data (exit 1) exit: 1 Then the fixture is "fixed" (RLS enabled, bucket locked) and the same scan is CLEAN:
$ vibeleak scan <app-url> --i-own-this-app --out report-after.json scanning http://127.0.0.1:37311 ... project: http://127.0.0.1:37311 (Supabase URL found in shipped JavaScript) anon key: found (role anon) tables: 3 enumerated via OpenAPI orders: 0 row(s) visible profiles: 0 row(s) visible public_posts: 1 row(s) visible, columns: id, title, body (allowlisted: public by design) storage buckets: 3 uploads: not listable public-assets: object listing allowed (allowlisted) private-files: not listable verdict: CLEAN - anonymous read clean (not a full audit; see Honest limits) $ vibeleak check --report report-after.json --baseline report-before.json checking report-after.json against baseline report-before.json ... public_posts still public by design (allowlisted) no regressions vs baseline verdict: CLEAN - anonymous read clean (not a full audit; see Honest limits) $ vibeleak diff --baseline report-before.json --report report-after.json private-files: CLEAN -> CLEAN public-assets: ALLOWLISTED -> ALLOWLISTED uploads: LEAK -> CLEAN (fixed) orders: CLEAN -> CLEAN profiles: LEAK -> CLEAN (fixed) public_posts: ALLOWLISTED -> ALLOWLISTED 2 fixed, 0 new leaks, 0 regressions The full transcript (54 lines, real CLI output) and the GIF are in demo/ in the repo. The test suite is 47/47 passing (36 unit + 11 e2e, including consent refusal with zero requests sent, service_role refusal before any request, redaction of row values from reports, and baseline diff regressions). CI is green on Node 20 and 22.
Install (from source only)
The npm package is not published yet, so for now install from source:
git clone https://github.com/techdevfun/vibeleak.git cd vibeleak npm install npm run build node dist/cli.js --help Or link the binary locally:
npm link vibeleak --help Requires Node.js 20 or newer, zero runtime dependencies.
How it compares
Checked 15 November 2026. The wedge is the first row: no other tool starts from only the deployed app URL, extracts the Supabase URL and anon key from shipped JavaScript, and probes anonymously.
| Tool | What it does well | Where it is weak (the wedge for vibeleak) |
|---|---|---|
| supabase-rls-check (Python CLI, consent-gated; PyPI, v0.6.3 announced 14/11/2026) | probe with an anon key via PostgREST; scan-env finds service_role keys in code; --fail-on for CI; refuses service_role before requesting | Catalog checks need a manual round-trip: export check.sql, paste it into the SQL Editor, export a CSV, then analyse. probe needs the user to supply the project URL and anon key. No baseline diff for anonymous exposure in one command. |
rls-doctor (Node CLI, check/probe/shadow/prove; npm 0.5.8, published 11/10/2026) | Behavioural probe: impersonates app roles in a rolled-back transaction and counts rows per identity; deep catalog analysis (policy OR-combine, WITH CHECK fallback, owner bypass) | Requires DATABASE_URL, a direct Postgres connection. Lovable and vibe-code users usually do not have one. No anon-key-only mode, no start from a deployed app URL, no frontend bundle scan. |
| Lovable Security Scanner + AI Security Reviewer (in-platform, added after CVE-2025-48757) | Runs where the app was built | Lovable only. No CI run, no exported evidence, no use for Bolt, v0, or other Supabase apps. Lovable's own docs say the scan does not guarantee complete security. |
| Supabase Security Advisor (dashboard) | Flags tables with RLS disabled (rls_disabled_in_public) | Dashboard only. No CLI or CI, and it reads the catalog: it never proves what an anonymous request actually returns. |
Where rivals beat us
Honesty first: vibeleak owns a narrow slice, and the rivals are real.
- supabase-rls-check reads the catalog, so it catches subtle policy bugs (policy OR-combine, owner bypass, SECURITY DEFINER) that an anonymous probe cannot see. If you need to know why a policy is wrong, not just that a table reads open, it is the better tool for that question.
- rls-doctor proves cross-user reads (user A reading user B's row). vibeleak only proves anonymous reads. If your threat is one logged-in user reading another user's rows, rls-doctor answers it and vibeleak does not.
- Both rivals analyse views and functions. vibeleak does not. A
SECURITY DEFINERview or a function with its own RLS behaviour is invisible to an anonymous table probe.
vibeleak is the outermost layer: prove what a stranger can read. It complements, not replaces, a catalog audit. Use it as the gate on every deploy, and use the catalog tools for the deeper audit.
Honest limits
- A CLEAN verdict means anonymous reads are clean. It is not a full audit. It does not catch: cross-user reads (user A reading user B's row), subtle policy bugs, write paths (anonymous INSERT/UPDATE),
SECURITY DEFINERviews/functions, or RLS inside functions. For those, use a catalog audit tool (supabase-rls-check, rls-doctor). See the comparison above. - The demo and e2e tests are fixture-only. No probe was ever run against a real Supabase project. The fixture PostgREST is a faithful stand-in for the table-probe path, but it is not a real environment. By design: the tool is consent-gated, and there was no consenting real app to probe.
- Storage object listing is probed with GET (
GET /storage/v1/object/list/<bucket>), because the tool is read-only by design. Real Supabase object listing is a POST endpoint, so a GET probe can return 405 there. Treat a non-listable result as "not proven listable", not as "proven private". The bucket list itself (GET /storage/v1/bucket) always works. -
checkneeds a report file. It cannot run an internal probe fromvibeleak.jsonwhen no report exists. The intended CI flow isscanthencheck. A probe-from-config mode forcheckis on the roadmap. - Key extraction can miss. Obfuscated bundles, keys in lazy-loaded chunks, or a disabled OpenAPI document all lead to REVIEW (exit 3), not a false CLEAN. Use
probe --tableswith an explicit key when extraction fails. - A public table is not automatically a leak. Pricing pages and public posts are public by design: that is what the allowlist is for. But an allowlisted table with a sensitive-looking column name still triggers a REVIEW.
Why this shape
The November 2026 numbers are not a story about one bad app. They are a story about a missing gate: AI writes the app, nobody proves the database is locked, and the app ships. The same bug class was disclosed in March 2025 and repeated at a larger scale eight months later, because a one-off scan does not stop a regression. A gate that runs on every deploy does, if it proves the exact thing an attacker would do: make an anonymous request and count the rows.
If you ship an AI-built app with real user data, prove it is locked before it goes live, and prove it again on every deploy:
github.com/techdevfun/vibeleak
Feedback welcome in the repo issues. The roadmap (probe-from-config mode for check, a real POST storage probe, a tighter demo GIF) is in the README.



