← Writing
Security

The 7 security holes in every vibe-coded app

The feature work is fine. The security is almost always the same seven holes — and any one can end a company on launch day.

Christos MalamasJun 20268 min read

I tear down a lot of apps that founders built with AI in a weekend. The feature work is often genuinely good. The security is almost always the same seven holes — and any one of them can end a company on launch day.

This isn't a knock on AI coding. It's a knock on shipping what it gives you without a second pass. The model optimises for “make the feature work,” not “make it safe when a stranger pokes at it.” Here's what the data says, then the seven holes I find almost every time — each with the exact fix.

45%
of AI-generated code introduced a known vulnerability
Veracode 2025
2.74×
higher security-vuln rate in AI-co-authored code
vs human-only
170
Lovable apps leaked user data via missing Supabase RLS
CVE-2025-48757
Security-vulnerability rate by how the code was written
16%
Human-written
30%
AI-assisted
44%
Fully AI-generated
Directional figures synthesised from 2025 Veracode and academic studies. The trend, not the decimal, is the point: the more the code is generated, the more holes ship with it.

01Secrets shipped to the browser

Critical — instant compromise

The number one finding, every time. A secret key — a service token, an admin key, a database URL — sits in the frontend bundle. AI tools love putting things in NEXT_PUBLIC_variables because it “just works” — but anything prefixed that way is shipped to every visitor's browser. (Publishable / anon keys are meant to be public — it's the secret ones that end careers.) I find these in under four minutes with the Network tab open.

Leaks to every visitor
1// .env.local-NEXT_PUBLIC_OPENAI_KEY=sk-proj-...-NEXT_PUBLIC_STRIPE_SECRET=sk_live_...4 5// client component6const res = await openai.chat(...)
Stays on the server
1// .env.local (no NEXT_PUBLIC prefix)+OPENAI_KEY=sk-proj-...3 4// app/api/chat/route.js — server only+const res = await openai.chat(...)6// browser calls /api/chat, never sees the key

02Authorisation that only exists in the UI

Critical — full data access

The app hides the “Delete” button unless you're an admin. But the API endpoint behind it never checks who is calling. Hiding a button is not security — anyone can call the endpoint directly with curl. The check has to live on the server, on every request.

Trusts the client
1export async function DELETE(req) {-  // no check — UI 'hides' this3  await db.users.delete(req.body.id)4}
Checks on the server
1export async function DELETE(req) {+  const me = await getSession(req)+  if (me?.role !== 'admin')+    return res(403)5  await db.users.delete(req.body.id)6}

03Supabase / Firebase left unprotected

Critical — your whole database is public

Managed databases hand your frontend a public key — Supabase's anon key, Firebase's config — that's meantto ship in browser code. It's safe only if you've turned on access rules that say who can read what.

The trap is real. In Supabase, tables created via raw SQL have Row Level Security off, and a table is live to the public API the moment it exists — so the public key reads every row. Firebase has its own version: pick test mode to move fast and the database is open to the world for 30 days. Miss either, and anyone with the public key reads every user and every record.

04No rate limiting anywhere

High — abuse & runaway cost

Generated code rarely adds rate limits. On a login route that means unlimited password guesses. On anything wrapping a paid API — AI, email, SMS — it means one script can run your bill to four figures overnight.

middleware.jsjs
1// a basic limiter is 6 lines and stops the worst abuse2const hits = new Map()3export function rateLimit(ip, max = 10, windowMs = 60_000) {4  const now = Date.now()5  const log = (hits.get(ip) || []).filter(t => now - t < windowMs)6  log.push(now); hits.set(ip, log)7  return log.length <= max8}

05Trusting whatever the client sends

High — privilege escalation

Two classics. IDOR: the endpoint takes anid from the request and returns that record without checking you own it — change the number, read someone else's data. Mass assignment: you spread the whole request body into a database write, so a user can send role: "admin"and promote themselves.

Spreads raw input
1await db.users.update(id, {-  ...req.body  // role, isPro, anything3})
Allow-lists fields
+const { name, bio } = req.body2await db.users.update(id, {+  name, bio  // only what's allowed4})

06Error messages that hand over the map

Medium — recon for attackers

Unhandled errors return full stack traces, SQL strings, and file paths to the client. Each one tells an attacker exactly what stack you run and where to push. Catch errors, log the detail server-side, return a generic message.

07API routes with no authentication at all

Critical — open doors

The frontend is behind a login, so it feels protected — but the API routes underneath often have zero auth. They're reachable directly, no session required. Every route that touches data needs to verify the session itself, not assume the UI gated it.


The browser is hostile territory. Anything it can see is public, and anything it can send, it will lie about. Security lives on the server or it doesn't exist.

The rule I give every founder I tear down
Ship-day checklist
  • No secret keys in the frontend — grep your bundle for sk_, service_role, and NEXT_PUBLIC_
  • Every data endpoint checks the session on the server, not the UI
  • Row Level Security on, with a policy per table
  • Rate limits on auth and any paid-API route
  • Allow-list fields on writes; verify ownership on reads
  • Generic error messages to the client, full detail in your logs only

None of this slows you down once it's habit — it's an afternoon, not a rebuild. And it's the difference between a launch and a breach post on the day you wanted to celebrate.

Found this useful?

We'll find your biggest launch risk. Free. In 48 hours.

Send your app — get a 3–5 minute video from a senior founder-engineer showing the one thing most likely to break trust, and how to fix it.

Get a free teardown →