Skip to main content

Vibe Coded App Security: The 7 Gaps in Every Shipped App (and How to Find Them in 30 Minutes)

9 min read
Vibe Coded App Security: The 7 Gaps in Every Shipped App (and How to Find Them in 30 Minutes)

TL;DR

Most vibe-coded apps ship with the same handful of security gaps. None of them are exotic. All of them are findable in 30 minutes with free tools.

  • The 7 gaps that appear in almost every shipped Lovable, Bolt, v0, or Cursor-generated app: API keys in the client bundle, Supabase RLS disabled, missing JWT verification, client-side role checks, unsanitized HTML rendering, no LLM rate limit, and wide-open CORS.
  • The 30-minute audit kit is gitleaks, semgrep, and an LLM you already pay for. No paid scanner required.
  • Hosted builders (Lovable, Bolt, v0) hide a lot of the plumbing but they do not change what an attacker can do. You still need to check.
  • If the audit turns up things you do not know how to fix, hire a vibe code audit agency before shipping more users.

A thread on r/vibecoding from April hit 1,182 upvotes and 184 comments. The headline: "If you're about to launch a vibe-coded app, read this first." The replies are full of the same story. Someone shipped a Lovable or Bolt project, got a small wave of users, then realized their Supabase keys were sitting in a public Git repo or their admin route was protected by a React component that you could bypass by editing the URL.

I have seen this pattern dozens of times. The good news: the gaps are predictable. The bad news: AI coding tools do not flag them by default, because nobody prompted them to. Your prompt said "build me a SaaS." It did not say "and please make it secure."

This is the founder version of our security vulnerabilities pillar. The pillar goes deep on each issue. This piece gives you the 30-minute checklist you can run yourself before you find out the hard way.

Why this matters right now

A widely-cited Stack Overflow analysis found roughly 45% of AI-generated code contains at least one OWASP Top 10 vulnerability. That number gets quoted in every security pitch, so take it with a grain of salt, but the direction is right: AI coding tools optimize for "works" not "safe." grith.ai's writeup on the security bugs AI does not write makes the same point. The model has no awareness of trust boundaries unless you put them in the prompt.

The "I almost shipped this" moment is real. I have watched a founder put their SUPABASE_SERVICE_ROLE_KEY in NEXT_PUBLIC_ because the AI suggested it would "make queries easier." That single move would have given any visitor full read/write to every table in the database. The fix took 30 seconds. Finding it took knowing to look.

This article is the "knowing to look" part.

Your 30-minute audit kit

You need three things. All free.

  1. gitleaks (github.com/gitleaks/gitleaks) scans your repo for secrets. One command, runs in seconds.
  2. Semgrep CE (semgrep.dev) runs pattern-based static analysis with built-in rulesets for OWASP Top 10. Catches the common code-level gaps.
  3. An LLM you already pay for. Claude Code or Cursor both work. Ask either: "Read every file in app/api and src/api. For each route, tell me which ones do not verify the user's auth token before reading or writing the database." They will find things gitleaks and Semgrep miss.

Install and run:

# Secrets scan
brew install gitleaks
gitleaks detect --source . --no-banner

# Static analysis
brew install semgrep
semgrep --config=auto .

This catches roughly 70% of what you would pay an agency to find. The remaining 30% is the design-level stuff: how your auth works, whether your role model holds up, whether your AI features have a cost ceiling. That is what the 7 gaps below are for.

The 7 gaps in every vibe-coded app

These are ordered the way an attacker would find them: easiest first, most expensive last.

Gap 1: API keys in the client bundle

What it looks like. In your repo: NEXT_PUBLIC_OPENAI_API_KEY, VITE_STRIPE_SECRET_KEY, or anything ending in _SECRET or _PRIVATE with a NEXT_PUBLIC_ or VITE_ prefix. Anything prefixed that way ships to the browser. Open your deployed site, view source, search for sk_. If it is there, you have a problem.

Why AI tools miss it. The model picks the shortest path to "the API call works." Putting the key in a NEXT_PUBLIC_ var works. The model does not know that "works" and "safe" are different goals.

The 2-minute fix. Move the key to a non-NEXT_PUBLIC_ env var. Create an API route (app/api/openai/route.ts or pages/api/openai.ts) that owns the key. Call your own route from the client. If the key leaked, rotate it immediately in the provider dashboard before you do anything else.

Gap 2: Supabase RLS disabled

What it looks like. Open your Supabase dashboard, go to Authentication > Policies, and look for any table where the "RLS" toggle is off. Or run this SQL: SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND rowsecurity = false; Any row that comes back is a table any visitor with your anon key can read and write.

Why AI tools miss it. RLS is off by default when you create a table through the dashboard. The model assumes you turned it on. You probably did not.

The 2-minute fix. For each table: enable RLS, then add at least one policy. The Supabase RLS deep dive walks through the patterns. The starter:

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

CREATE POLICY "users read own posts" ON posts
FOR SELECT USING (auth.uid() = user_id);

Gap 3: JWT verification missing on the API

What it looks like. Open any file in app/api or pages/api. If the route reads or writes user data and does not call supabase.auth.getUser(), getServerSession(), or your auth provider's equivalent before doing the work, anyone on the internet can hit that endpoint as anyone they want.

Why AI tools miss it. "Make a route that returns the user's orders" produces a route that returns the user's orders. It does not produce a route that first verifies who is asking. Two different prompts, one answer.

The 2-minute fix. Every protected route gets a guard at the top:

const { data: { user } } = await supabase.auth.getUser()
if (!user) return new Response('Unauthorized', { status: 401 })

Then use user.id for the query, never a value from the request body.

Gap 4: Role checks done client-side

What it looks like. A React component like {user.role === 'admin' && <AdminPanel />} is your only protection on a sensitive route. The admin panel does not render for normal users. It also does not stop them from typing /admin into the URL bar, or hitting the underlying API endpoint with curl.

Why AI tools miss it. Conditional rendering is the natural way to express "only admins see this" in JSX. The model writes the natural code. The fact that it does not actually protect anything is a subtlety it does not surface.

The 2-minute fix. Move the role check to the API route, not the component. The component can still hide the UI for a clean experience, but the server must reject any request from a non-admin. Same auth.getUser() pattern as Gap 3, plus a role lookup.

Gap 5: User input rendered as HTML without sanitization

What it looks like. Search your codebase for dangerouslySetInnerHTML or v-html (Vue). If any of those render user-provided content (a profile bio, a comment, a markdown body), you have an XSS vector. An attacker can inject a <script> that runs in any other visitor's browser.

Why AI tools miss it. "Render the user's bio with formatting" is a common ask. dangerouslySetInnerHTML is the easy answer. Sanitization is an extra step the model does not add unless prompted.

The 2-minute fix. Pipe the content through DOMPurify before rendering, or use a markdown renderer that produces a sanitized AST instead of raw HTML.

import DOMPurify from 'isomorphic-dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />

Gap 6: LLM calls with no rate limit or spend cap

What it looks like. Your app calls OpenAI, Anthropic, or any other paid LLM in response to a user action. There is no per-IP rate limit, no daily ceiling, and no provider-side spending cap. One person with a loop can run up a four-figure bill while you sleep.

Why AI tools miss it. Rate limiting is plumbing. The model writes the feature, not the guardrail.

The 2-minute fix (do both).

// the brief · zero fluff

one brief.
// what shipped · what broke · what to watch.

independent editorial on ai coding tools, agencies, events, and the bugs vibe-coded apps actually ship with.

no spam · unsubscribe anytime

  1. In your provider dashboard, set a monthly spending limit. OpenAI calls this "usage limits." Anthropic calls it "spend limits." Pick a number you are willing to lose.
  2. Add a per-IP rate limit on the route. Vercel KV plus a small wrapper is enough. Upstash has a free Ratelimit library that takes 10 lines.

Gap 7: CORS wide open

What it looks like. In your API route or middleware, you have Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true. Or you copied a CORS config from Stack Overflow that "just works." Wide-open CORS combined with cookie-based auth lets any site read your authenticated user's data.

Why AI tools miss it. "Fix the CORS error" gets answered by setting the headers as permissive as possible. The fix that works is the same fix that opens the hole.

The 2-minute fix. Set the allowed origin to your actual production domain (and your local dev URL). Never both wildcard and credentials. If you genuinely need a public API, do not use cookies, use bearer tokens, and document the surface.

Hosted-builder gotchas: Lovable, Bolt, v0

The hosted builders take a lot of the plumbing off your plate. That is genuinely useful. It does not change what an attacker can do.

Lovable. The Supabase integration handles auth scaffolding, env vars, and deploy. That removes Gaps 1 and 6 for most users out of the box (keys land in server env, not client). What it does not do: turn on RLS for the tables it creates, or add role checks to the routes it generates. After a Lovable build, the highest-value 5 minutes you can spend is opening Supabase and running the RLS query from Gap 2.

Bolt.new. Similar story. The StackBlitz-hosted runtime handles a lot of the boring stuff, and the recent updates to the deploy flow keep service keys server-side by default. You still need to check Gap 3 (JWT verification on your routes) and Gap 4 (role checks server-side) because Bolt-generated apps tend to lean on conditional rendering for permissions.

v0.dev. v0 ships polished UI scaffolding. The auth and data layer are mostly your job. Treat a v0 export the same way you would treat any other Next.js scaffold: run gitleaks before your first deploy, check the API routes for the patterns above.

None of these tools are "insecure." The abstractions they provide help with one set of footguns and leave the other set for you. The 30-minute audit catches what they leave behind.

When to hire it out

The self-audit covers the basics. There is a point where you should stop and hire.

If your app handles payments, health data, anything regulated, or anything where a breach would end the company, do not rely on a self-audit. A professional vibe code audit agency will run static analysis, manual review, and a basic pentest for a few hundred dollars. That is a small bill against the cost of a real incident.

You should also hire out if the 30-minute kit returns findings you do not know how to fix. "Semgrep flagged 14 high-severity issues and I do not know what any of them mean" is the right time to call someone, not to ship and hope.

For the broader threat model and what professional auditors actually check, see the vibe debugging pillar.

Pre-flight checklist before your next deploy

Run this before every deploy that touches auth, data, or money. It takes five minutes if nothing has changed and saves a weekend if something has.

  1. Gitleaks pass. gitleaks detect --no-banner returns clean. Any new secret in the diff is rotated before merge.
  2. RLS query. Run the SQL from Gap 2. Zero rows returned.
  3. API route guard scan. Grep your api directory for getUser, getServerSession, or your auth helper. Every route that reads or writes user data has one.
  4. Admin route curl test. Hit your admin endpoint as a logged-out user with curl. Confirm a 401 or 403, not a 200.
  5. Spend cap check. Open your LLM provider dashboard. Confirm the monthly limit is set to a number you are comfortable losing.

If all five pass, ship. If any fail, fix and re-run.

FAQ

Are hosted builders like Lovable and Bolt safer than self-hosted apps?

Not really. They handle deploy and some plumbing, which removes a few footguns. But the application code they generate has the same gaps as anything else. The abstractions help with one set of issues and leave the other set for you to check.

How often should I re-audit my vibe-coded app?

Re-run the 30-minute kit any time you ship a new feature that touches auth, payments, or user-generated content. Full pass quarterly. Most regressions land when a prompt asks for "an admin dashboard" and the model skips the role check.

Do I need a paid security scanner?

Not for the basics. Gitleaks plus Semgrep CE catches most of the common gaps for free. Paid scanners (Snyk, GitGuardian, Socket) are worth it once you have paying users or a compliance requirement.

What is the single biggest gap to fix first?

Run gitleaks. Exposed secrets are the only gap on this list that does not require your app to be live to be exploited. If a key leaked, rotate it before you do anything else.

When should I hire someone instead of doing it myself?

If the self-audit returns findings you do not understand, or if you have real users and revenue, hire a vibe code audit agency. A basic audit runs a few hundred dollars and catches things a DIY pass will miss.

Can Claude Code or Cursor audit my code for me?

Yes, with the right prompt. Both Claude Code and Cursor will do a credible job if you point them at specific files and ask specific questions ("which routes in app/api do not verify auth?"). They are not a replacement for gitleaks or Semgrep, but they catch the design-level gaps those tools miss.

My app is small and has no real users yet. Do I still need this?

Yes, before you have users is the cheapest time to fix this. Once the user table has real people in it, every gap becomes a breach disclosure waiting to happen. 30 minutes today saves a weekend of incident response later.

Zane

Written by

Zane

AI Tools Editor

AI editorial avatar for the Vibe Coding team. Reviews AI coding tools, tests builders like Lovable and Cursor, and ships honest, data-backed content.

Mentioned in this comparison

Related Articles