CheckVibeCodeCheckVibeCode
All posts
SecurityJuly 14, 20269 min read

The 7 security mistakes every vibe-coded app makes

C

CheckVibeCode Team

Product & Security Research

Ask an AI page builder for a working app and you'll get one — fast. Ask it for a secure one, and it'll hand you the same code, because nobody asked it the second question. We've scanned thousands of vibe-coded sites, and the same handful of gaps show up in almost all of them. None require a security background to fix. Most take under ten minutes.

1. API keys shipped straight to the browser

The fastest way to get a feature working is to call the third-party API directly from the client — and the fastest way to do that is to paste the key into an environment variable that the framework happily bundles into the page source. We find live, working keys — Stripe, OpenAI, Resend, Mapbox — in public JavaScript bundles on a majority of the sites we scan for the first time.

If it starts with NEXT_PUBLIC_ or VITE_, assume it ships to every visitor's browser — because it does.

The tell is almost always the same: open DevTools, look at the Network tab or the compiled JS chunk, and search for sk_, AIza, re_, or _secret. It takes about thirty seconds to find one on a site that has it, which is roughly how long it takes an attacker too. The fix isn't "be more careful" — it's moving the call server-side, behind an API route the key never leaves.

2. No Row Level Security on tables that need it

Supabase and Firebase make it trivial to query a table straight from the client. That's also what makes an unprotected table trivial to read — or write — from anyone's browser console. A missing RLS policy on a users, orders, or messages table is the single most common critical finding in our security level.

The exploit path is almost embarrassingly simple: copy the anon key out of the client bundle (it's meant to be public, that's not the bug), open a console, and run supabase.from('users').select('*'). If RLS is off, that returns every row — emails, in some cases password reset tokens or Stripe customer IDs — to anyone who thought to look. No injection, no auth bypass, just an unlocked table.

3. Auth checks that only run on the frontend

Hiding an admin button behind an if (user.isAdmin) in React stops the button from rendering. It does nothing to stop a request to the underlying API route or Supabase table — those need their own checks, server-side, every time.

This is easy to prove to yourself: find any "admin-only" action in a vibe-coded app, open the Network tab, copy the request as curl, and re-send it without the admin session. If it still succeeds, the check was decorative. The frontend can decide what to show; only the server can decide what to allow.

4. Default CORS and permissive headers

Scaffolded backends are usually generated wide open — Access-Control-Allow-Origin: *, no Content-Security-Policy, no HSTS. Fine for a local demo, risky the moment real user data is involved.

Wildcard CORS paired with cookie-based auth is the combination that actually bites: it means any site on the internet can point a fetch request at your API from a logged-in user's browser and have the cookie ride along. Scope Access-Control-Allow-Origin to your real domains, and never pair * with Access-Control-Allow-Credentials: true — most frameworks will refuse to combine them anyway, which is your first hint something's wrong.

5. Debug and admin routes left in production

/api/debug, /admin, /test-webhook — routes that were useful while building and were never gated behind auth or removed before launch. We check for the common ones automatically; it's worth a manual pass too.

The pattern we see most is a seed or reset route — /api/seed-db, /api/reset — that an AI assistant generated to make local testing faster, complete with a comment like // TODO: remove before prod that nobody circled back to. Grep your routes for seed, debug, test, and reset before you ship, not after someone else finds them first.

6. Dependencies nobody has looked at

AI-generated projects tend to pull in more packages than a hand-rolled one, because it's cheaper for the model to add a library than to write ten lines. More dependencies means a larger, mostly-unaudited attack surface — and known CVEs in transitive packages are common.

We regularly see three date-formatting libraries in one package.json, or a full animation framework pulled in for a single fade-in. Each one drags its own dependency tree with it — a five-line feature can quietly add forty transitive packages, any one of which can carry a CVE you never chose to accept.

7. No monitoring for when something breaks

Even a well-secured app drifts — a dependency gets a CVE, a certificate nears expiry, an endpoint starts getting probed. Without something watching continuously, the first sign of trouble is usually a customer complaint, not an alert.

A site that passed every check on launch day can fail half of them six weeks later without a single line of code changing — a new CVE lands in a dependency you didn't touch, or a header your host used to send by default quietly stops being sent after a platform update. Security is a state you have to keep re-checking, not a box you tick once.

How we grade severity

Not all seven are equally urgent, and treating them that way just trains people to ignore the list. We rank a finding critical when it exposes user data with no auth required at all — an open RLS table, a leaked service-role key. High covers things exploitable with minimal effort, like a wildcard CORS policy on an authenticated endpoint. Medium and low are real but need more work to exploit, or affect data that isn't sensitive on its own — a missing security header, a stale dev dependency with no known exploit path yet.

  • Grep your client bundle for anything that looks like a secret before you ship.
  • Turn on Row Level Security by default, then open it up per-table on purpose.
  • Re-check auth on the server for every route that touches user data.
  • Set a real Content-Security-Policy instead of leaving it unset.
  • Delete debug routes, or put them behind auth.
  • Run a dependency audit before launch, not after an incident.
  • Scope CORS to real origins, and never combine a wildcard with credentialed requests.
  • Put something on a schedule that checks your site again next week.

You can go check for all seven by hand, or point a scanner at the URL and get the same list back in under a minute — ranked by severity, with a fix prompt attached to each one.

See where your own site stands.

Run a free scan and get the same checks this post talks about, applied to your app.