// private beta · onboarding builders in wavesrequest_access →
← Back to blog

Vibe coding security: the seven holes AI-generated apps ship with

Vibe coding security: the seven holes that recur in AI-generated apps, from browser-side paywalls to open RLS, and a 20-minute audit to find them in yours.

Vibe-coded apps ship with the same seven security holes: authorization in the browser, secrets in the repo, row-level security off, input taken on faith, no rate limiting, CORS open to every origin, and error pages that narrate your internals. Each one takes minutes to check for, and this guide shows how.

AI doesn't write insecure code randomly. It writes the same insecure code, over and over, because it learned from a decade of tutorials and demos where security was somebody else's chapter. That's actually good news: a short, specific list covers most of what will bite you.

We run coding agents against real repositories every day, and nothing they write lands until it passes deterministic checks and an independent review. The findings below are the ones that recur, ordered by how often they come up, with the way to check your own app in minutes.

1. Authorization that lives in the browser

The champion, and the most expensive. The model puts the permission logic where it's easiest to demo: the frontend. Admin panels hidden behind an if (isAdmin) in React. Paid features gated by component state. In March 2025 a founder announced a SaaS built entirely with Cursor and no hand-written code; two days later he reported users bypassing the subscription, maxing out his API keys, and writing junk into his database. The app was gone within the week.

Why AI does it: hiding a button is one line and makes the demo work. Server-side enforcement needs middleware, session checks, and route changes; nothing about "make an admin panel" forces any of that.

Check yours: open your app logged out (or as a free user) and call your API routes directly from the browser console or curl. If a route answers with data the UI wouldn't have shown you, you've found it.

The fix: every protected route verifies the session and the permission on the server, and the UI merely reflects what the server decided. No exceptions, including "just this internal endpoint." The same rule governs paid features and billing state.

2. Secrets committed to the repo

Stripe secret keys, OpenAI keys, database URLs, pasted into code during a prompting session and immortalized in git. If the repo is public, scrapers find keys in minutes; if it's private, they wait until the day it isn't.

Check yours:

git grep -nE "sk_(live|test)|service_role|API_KEY.*=" -- '*.ts' '*.tsx' '*.js' '*.py'

The fix: environment variables for everything, a .env in .gitignore, and rotate any key that was ever committed. Rotation is free. The history remembers even after you delete the line.

3. Row-level security off (the Supabase special)

Builder tools lean on Supabase, where the client talks to the database directly and row-level security is the mechanism keeping user A out of user B's rows. Demos work fine with RLS off, so generated projects ship with it off, or with a policy like USING (true) that waves everyone through. Result: any user can read the whole table through the public API, politely and without hacking anything.

Check yours: in the Supabase dashboard, every table containing user data should show RLS enabled, and each policy should mention auth.uid(). Then verify behaviorally: sign in as user A, try to fetch user B's row by id.

The fix: enable RLS on every table, write per-user policies, and re-test. Supabase's own docs walk through it; the work is an hour, the omission is a breach notification. If your app came out of Lovable, this is step two of what to do after building with Lovable.

4. Input handled on faith

Generated endpoints tend to trust whatever arrives: unvalidated JSON written straight to the database, user content rendered without sanitization (hello, stored XSS), file uploads with no type or size limits. Each is an old vulnerability made new again by code nobody read.

Check yours: paste <script>alert(1)</script> into every text field you have and see where it executes. Upload a 300MB file. Send a negative number where a price goes.

The fix: validate on the server with a schema library (Zod, Pydantic), sanitize anything rendered, cap uploads. Ask your AI tool to add validation route by route; it's good at this when told explicitly.

5. No rate limiting anywhere

Login endpoints that accept a thousand attempts a minute, AI features that pass your OpenAI bill to whoever loops a request, signup flows open to scripts. Abuse prevention never appears in demos, so it never appears in generated apps.

Check yours: hit your login or your most expensive endpoint 50 times in a loop. If all 50 land, so will someone else's 50,000.

The fix: a rate-limiting layer at the edge (most hosts and API gateways offer one) plus per-user caps on anything that costs you money per call.

6. CORS set to *

Faced with a cross-origin error, generated code reaches for the fix that always works: allow every origin, with credentials. That lets any website a user visits call your API as them. It's one config line, and it's load-bearing.

Check yours: look for Access-Control-Allow-Origin: * (or an origin: true in your framework's CORS config) on authenticated routes.

The fix: allow exactly your domains. The error the model was dodging was telling you something true.

7. Error pages that narrate your internals

Stack traces, SQL fragments, and file paths returned to the client, because debug mode made development easier and nobody flipped it off. Attackers read error messages the way burglars read floor plans.

Check yours: force an error (malformed id, missing field) in production and read what comes back.

The fix: generic errors to the client, full detail to your logging tool.

The 20-minute audit, all together

  • Call protected API routes logged out; expect denials
  • git grep for keys; rotate anything found
  • RLS on, policies use auth.uid(), tested as two users
  • Script tag in every field; oversized upload; absurd numbers
  • 50 rapid requests against login and expensive endpoints
  • CORS lists your domains, not *
  • Production error pages say nothing interesting

Twenty minutes, honestly spent, puts you ahead of a large share of launched vibe-coded apps. Then the deeper truth kicks in: security isn't a launch task but an operating condition. Dependencies sprout CVEs, keys age, new endpoints ship without the checklist. That recurring layer belongs to production operations, wherever you decide those live, and the production readiness checklist is where to write it down for any SaaS.

Solo's scaffold starts on the right side of this audit. Server-side auth, a validated API, and secrets kept out of the repo come with the standard stack, and every change that lands afterwards, yours or Solo's, passes the same checks and an independent review. You build features; the floor stays solid. Join the waitlist →

Keep reading