{"slug": "24664-ai-built-apps-sent-database-records-to-any-visitor-i-built-a-one-command", "title": "24,664 AI-built apps sent database records to any visitor. I built a one-command check that proves yours does not.", "summary": "A developer built vibeleak, a zero-dependency TypeScript CLI that checks whether AI-built Supabase apps leak database records to anonymous visitors. The tool extracts the Supabase project URL and anon key from an app's shipped JavaScript, enumerates tables via the PostgREST OpenAPI document, and probes each with a one-row anonymous GET, returning a CLEAN, LEAK, or REVIEW verdict with a JSON report and baseline diff for CI. The release follows Crissy Field and Handelsblatt research finding that 24,664 of 34,545 scanned Lovable apps using Supabase sent database records to the browser on page load without login.", "body_md": "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.\n\nThe 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.\n\nRead those numbers honestly, because the researchers are explicit about the limits:\n\nThe 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\".\n\nI built a small tool that does exactly that. It is called [vibeleak](https://github.com/techdevfun/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.\n\nWhen 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.\n\nNobody 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.\n\nThat 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.\n\nFive commands:\n\n```\nvibeleak scan <app-url> [--i-own-this-app] [--out report.json]\nvibeleak probe --url <project-url> --anon-key <key> [--tables a b]\nvibeleak check [--report report.json] [--baseline baseline.json]\nvibeleak diff --baseline baseline.json [--report report.json]\nvibeleak init [--dir .]\n```\n\n`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 a `service_role` JWT or `sb_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 the `vibeleak.json` allowlist. 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 sample `publicTables` allowlist, `failOn`) and an empty baseline. It never writes a key into a file: keys come from a flag or `VIBELEAK_ANON_KEY` and are never logged.\nVerdict rules, evaluated in order:\n\n`password`, `secret`, `ssn`, `passport`, `token`, `api_key`, name match only, values never read) - MEDIUM, REVIEW.`--tables`) - LOW, REVIEW.\nOnly 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.\n\nA typical CI flow:\n\n```\nvibeleak scan https://my-app.example.com --i-own-this-app --out report.json\nvibeleak check --report report.json --baseline vibeleak.baseline.json --fail-on high\nvibeleak diff --baseline vibeleak.baseline.json --report report.json\n```\n\nThe 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.\n\nThe leaky fixture scans LEAK:\n\n``` bash\n$ vibeleak scan <app-url> --i-own-this-app --out report-before.json\nscanning http://127.0.0.1:37965 ...\nproject: http://127.0.0.1:37965 (Supabase URL found in shipped JavaScript)\nanon key: found (role anon)\ntables: 3 enumerated via OpenAPI\n  orders: 0 row(s) visible\n  profiles: 1 row(s) visible, columns: id, email, passport_no\n  public_posts: 1 row(s) visible, columns: id, title, body (allowlisted: public by design)\nstorage buckets: 3\n  uploads: object listing allowed\n  public-assets: object listing allowed (allowlisted)\n  private-files: not listable\nfindings:\n  LEAK: profiles (1 row visible, columns: id, email, passport_no) - anonymous GET returns 1 row(s); table is not allowlisted\n  LEAK: uploads - anonymous object listing is allowed; bucket is not allowlisted\nverdict: LEAK - an anonymous visitor can read data (exit 1)\nexit: 1\n```\n\nThen the fixture is \"fixed\" (RLS enabled, bucket locked) and the same scan is CLEAN:\n\n``` bash\n$ vibeleak scan <app-url> --i-own-this-app --out report-after.json\nscanning http://127.0.0.1:37311 ...\nproject: http://127.0.0.1:37311 (Supabase URL found in shipped JavaScript)\nanon key: found (role anon)\ntables: 3 enumerated via OpenAPI\n  orders: 0 row(s) visible\n  profiles: 0 row(s) visible\n  public_posts: 1 row(s) visible, columns: id, title, body (allowlisted: public by design)\nstorage buckets: 3\n  uploads: not listable\n  public-assets: object listing allowed (allowlisted)\n  private-files: not listable\nverdict: CLEAN - anonymous read clean (not a full audit; see Honest limits)\n\n$ vibeleak check --report report-after.json --baseline report-before.json\nchecking report-after.json against baseline report-before.json ...\npublic_posts still public by design (allowlisted)\nno regressions vs baseline\nverdict: CLEAN - anonymous read clean (not a full audit; see Honest limits)\n\n$ vibeleak diff --baseline report-before.json --report report-after.json\nprivate-files: CLEAN -> CLEAN\npublic-assets: ALLOWLISTED -> ALLOWLISTED\nuploads: LEAK -> CLEAN (fixed)\norders: CLEAN -> CLEAN\nprofiles: LEAK -> CLEAN (fixed)\npublic_posts: ALLOWLISTED -> ALLOWLISTED\n2 fixed, 0 new leaks, 0 regressions\n```\n\nThe 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.\n\nThe npm package is not published yet, so for now install from source:\n\n```\ngit clone https://github.com/techdevfun/vibeleak.git\ncd vibeleak\nnpm install\nnpm run build\nnode dist/cli.js --help\n```\n\nOr link the binary locally:\n\n```\nnpm link\nvibeleak --help\n```\n\nRequires Node.js 20 or newer, zero runtime dependencies.\n\nChecked 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.\n\n| Tool | What it does well | Where it is weak (the wedge for vibeleak) | \n|---|---|---|\n| supabase-rls-check (Python CLI, consent-gated; [PyPI](https://pypi.org/project/supabase-rls-check/) , 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. | \n| rls-doctor (Node CLI, `check` /`probe` /`shadow` /`prove` ;[npm 0.5.8, published 11/10/2026](https://www.npmjs.com/package/rls-doctor) ) | 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. | \n| 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. | \n| 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. | \n\nHonesty first: vibeleak owns a narrow slice, and the rivals are real.\n\n`SECURITY DEFINER` view or a function with its own RLS behaviour is invisible to an anonymous table probe.\nvibeleak 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.\n\n`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.` probe --tables` with an explicit key when extraction fails.\nThe 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.\n\nIf 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:\n\n[github.com/techdevfun/vibeleak](https://github.com/techdevfun/vibeleak)\n\nFeedback 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.", "url": "https://wpnews.pro/news/24664-ai-built-apps-sent-database-records-to-any-visitor-i-built-a-one-command", "canonical_source": "https://dev.to/techdevfun/24664-ai-built-apps-sent-database-records-to-any-visitor-i-built-a-one-command-check-that-proves-1870", "published_at": "2026-11-15 12:11:13+00:00", "updated_at": "2026-11-15 12:21:23.801542+00:00", "lang": "en", "topics": ["ai-safety", "ai-tools", "developer-tools", "ai-products"], "entities": ["vibeleak", "Supabase", "Lovable", "Crissy Field", "Handelsblatt", "CVE-2025-48757", "PostgREST", "Common Crawl"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/24664-ai-built-apps-sent-database-records-to-any-visitor-i-built-a-one-command", "markdown": "https://wpnews.pro/news/24664-ai-built-apps-sent-database-records-to-any-visitor-i-built-a-one-command.md", "text": "https://wpnews.pro/news/24664-ai-built-apps-sent-database-records-to-any-visitor-i-built-a-one-command.txt", "jsonld": "https://wpnews.pro/news/24664-ai-built-apps-sent-database-records-to-any-visitor-i-built-a-one-command.jsonld"}}