Notes · 29 October 2026 · Bardstar

5 ways AI-built apps leak user data (and the 10-minute fix for each)

In March 2025, Matt Palmer scanned 1,645 apps on Lovable's own "Launched" showcase. 170 of them (10.3%, 303 endpoints) let anyone read private tables with no login. That became CVE-2025-48757 [1][2]. The code worked in every demo; the data was public.

None of the 5 leaks below is exotic. Each takes 2 minutes to check and 10 to fix. [S] marks a vendor doc, [V] a verified write-up.

1. RLS off, or "enabled" with no real policy

How it happens. Supabase gives every app a publishable (anon) key by design. The key is not the problem. Supabase's docs are blunt: "A table in an exposed schema without RLS is readable and writable by any role with a grant on it. Enable RLS on every table in an exposed schema" [3]. AI builders create the table, wire the UI, and skip the policy. In the CVE scan, /rest/v1/users, /transactions and /subscriptions were readable from the homepage [1]. In February 2026 the same mistake at Moltbook exposed 1.5 million API tokens and 35,000 emails [4].

2-minute check. Copy the Supabase URL and publishable key out of your built JS bundle. Run curl "https://<ref>.supabase.co/rest/v1/<table>?select=*" -H "apikey: <key>". If rows come back without a user token, you are leaking.

Fix. alter table <t> enable row level security; then a policy like using (auth.uid() = user_id) on select/update/delete. Revoke default anon grants on private tables; adding a policy does not remove a grant [3]. Run Supabase's Security Advisor; Lovable now surfaces it before publish [5].

2. Service-role or secret keys in the client bundle

How it happens. The builder needs an admin query and writes createClient(url, SERVICE_ROLE_KEY) in the browser. That key "has the bypassrls attribute. Never use a secret key in the browser or expose it to customers" [3]. One scan of 29 Lovable, Bolt and v0 apps found a service_role key in client JavaScript in 1 of 4 [6].

2-minute check. grep -rE "sb_secret_|service_role|sk_live_|whsec_" dist/ .next/ on the production build. Also check for secrets in NEXT_PUBLIC_ or VITE_ env vars; the prefix ships them [7].

Fix. Rotate the key today. Move the admin call behind an edge function, Worker or API route that reads the secret from a server-only env var. Stripe secret keys (sk_live_) follow the same rule [8].

3. Webhooks that trust whoever calls them

How it happens. The Stripe webhook handler parses JSON and upgrades the user on checkout.session.completed. Nobody verifies the Stripe-Signature header, so anyone can POST that event and get a paid plan for free. Stripe's docs require constructEvent(rawBody, signature, endpointSecret) on the raw request body, with a 5-minute timestamp tolerance; a tolerance of 0 disables the replay check [8].

2-minute check. From a terminal, POST {"type":"checkout.session.completed","data":{"object":{"client_reference_id":"<your-user-id>"}}} to your webhook URL. If you get 200 and your account changes, it is broken.

Fix. Read the raw body (not the parsed one), call stripe.webhooks.constructEvent, return 400 on failure, and treat the event id as idempotent.

4. Public storage buckets

How it happens. In July 2025 the Tea app confirmed 72,000 user images were taken, including 13,000 selfies and photo IDs uploaded for verification, after 4chan users found an exposed store [9]. Uploads to a public bucket work instantly, so builders leave them. Supabase buckets are private by default; a public bucket is readable by anyone with the URL [10]. Firebase's default Storage rules require request.auth != null, but a rule edited to allow read: if true during testing opens every file [11].

2-minute check. Open any uploaded file URL in a private window. Firebase: Console > Storage > Rules, look for if true. Supabase: Storage > bucket > Public toggle.

Fix. Private bucket plus signed URLs with a short expiry, or storage policies scoped to the owner ((storage.foldername(name))[1] = auth.uid()) [10][11].

5. IDOR: the id in the URL is the only "auth"

How it happens. /api/invoices?id=1042 returns invoice 1042 for any logged-in user, because the query filters by id and not by owner. In the CVE scan, "session tokens that could be spoofed to access another user's account" was one of the 3 patterns [2]. At Base44, Wiz showed that a non-secret app_id sent to undocumented register and verify-OTP endpoints produced a verified account on private apps [12].

2-minute check. Log in as user A, take an object id from user B (or decrement an integer id), request it. Repeat for update and delete.

Fix. Every read and write filters by the authenticated user on the server or in RLS. Switch sequential ids to UUIDs, but treat that as defence in depth, not authorization.

What we do with this

These 5 checks are the first pass of every Bardstar review. We run them on a test copy of your app, prove each finding, and send the fixes as pull requests. If you built with Lovable, Bolt, v0, Cursor or Replit on Supabase or Firebase, start with the 5 above.

Sources

  1. [V] mattpalmer.io/posts/statement-on-CVE-2025-48757/ (1,645 scanned, 170 projects, 303 endpoints)
  2. [V] superblocks.com/blog/lovable-vulnerabilities; CVE record: nvd.nist.gov/vuln/detail/CVE-2025-48757
  3. [S] supabase.com/docs/guides/database/postgres/row-level-security
  4. [V] wiz.io/blog/exposed-moltbook-database-reveals-millions-of-api-keys
  5. [S] lovable.dev/blog/secure-vibe-coding
  6. [V] vibewrench.dev/security/supabase (29 apps; third-party scan)
  7. [S] supabase.com/docs/guides/api/api-keys
  8. [S] docs.stripe.com/webhooks and docs.stripe.com/keys-best-practices
  9. [V] techcrunch.com/2025/07/26/dating-safety-app-tea-breached-exposing-72000-user-images/
  10. [S] supabase.com/docs/guides/storage/buckets/fundamentals and /storage/security/access-control
  11. [S] firebase.google.com/docs/storage/security and /docs/rules/basics
  12. [V] wiz.io/blog/critical-vulnerability-base44