Skip to content
Muhammad Moiz

Vibe Coding Security: 8 Holes in Almost Every AI-Built App

Vibe coding security checklist: the 8 holes found in most Lovable, Bolt and Replit apps, how to test for each, and how to fix them.

Muhammad Moiz
Published
min read
9 min read
Red padlock resting on a black computer keyboard
On this page

Vibe coding security comes down to one problem: AI app builders are very good at making features work and much less careful about who is allowed to use them. The same handful of holes turn up again and again in AI-built apps, and most of them are about access control, leaked keys and trusting the browser. Below are the 8 that come up most often, how to check for each one in an afternoon, and what to do when you find them.

This is written for founders who built with Lovable, Bolt, Replit, Cursor or v0, and for developers who just inherited one of those projects.

Is Lovable safe? Why AI-built apps share the same weaknesses

The tools themselves are not the problem. The problem is the default architecture they produce. Most AI builders generate a React frontend that talks directly to a backend-as-a-service such as Supabase or Firebase. There is often no server of your own in between. That means the browser is the only place where "rules" live, unless someone deliberately writes them into the database.

This is not a theory. CVE-2025-48757, published on 30 May 2025, describes insufficient Row Level Security in sites generated by Lovable up to 15 April 2025, which let unauthenticated attackers read or write arbitrary tables. The CVE record gives it a CVSS 3.1 base score of 9.3 (critical). Lovable disputes the entry, and the record notes their position that each customer is responsible for protecting their own app's data.

That dispute is the key lesson for any founder: whoever's fault it is, your users' data is your responsibility. The AI tool will not be the one emailing your customers about a breach.

The 8 vibe coding security holes

1. Row Level Security is missing or does nothing

With Supabase, your publishable (anon) key ships inside the frontend. That is by design. Supabase's own docs say requests made with it are limited to whatever Row Level Security allows. So RLS is the actual lock on your data.

The common failures:

  • RLS is turned off on one or more tables in the public schema.
  • RLS is on, but a policy like using (true) lets everyone read everything.
  • Policies check that a user is logged in, but not that the row belongs to them.

A safer pattern for a table where each user owns their rows looks like this:

alter table invoices enable row level security;

create policy "owners read their invoices"
on invoices for select
to authenticated
using ((select auth.uid()) is not null and (select auth.uid()) = owner_id);

Supabase recommends the explicit is not null check because auth.uid() returns null for anonymous requests, and wrapping it in select lets Postgres evaluate it once per query instead of once per row. I covered this hole in depth in Supabase RLS explained.

2. Secret keys shipped to the browser

The Supabase secret key (formerly the service_role key) bypasses every RLS policy. It belongs on a server, in an Edge Function or in a scheduled job, never in frontend code, never in Git. A common way it ends up in client code: an AI assistant "fixes" a permissions error by swapping the publishable key for the secret one.

The same mistake happens with environment variables. In Next.js, any variable prefixed with NEXT_PUBLIC_ is inlined into the JavaScript bundle at build time, so anyone can read it in their browser's dev tools. Other build tools have similar public prefixes. If a payment provider secret, OpenAI key or database password has a public prefix, treat it as leaked: rotate it, then move the call that needs it to the server.

Quick check: open your live site, open dev tools, search the loaded JavaScript for sb_secret_, service_role and the start of any secret key from your payment or AI provider.

3. Security rules that only exist in the UI

Hiding the "Delete" button for non-admins is not security. Neither is a redirect on the admin page. Anyone can call your API, your Supabase tables or your server functions directly with a tool like curl.

The Next.js docs make this point clearly for Server Actions: an exported action can be called with a direct POST request, so the page-level login check does not protect it. Each action needs its own check.

'use server'

import { getSession } from '@/lib/session'
import { db } from '@/lib/db'

export async function archiveProject(projectId: string) {
  const session = await getSession()
  if (!session) throw new Error('Not signed in')

  const project = await db.project.findFirst({
    where: { id: projectId, ownerId: session.userId },
  })
  if (!project) throw new Error('Not found')

  await db.project.update({ where: { id: project.id }, data: { archived: true } })
  return { ok: true }
}

4. Trusting IDs sent from the client (IDOR)

This is the cousin of hole 3. The user is logged in, but the app takes an ID from the URL or request body and loads that record without asking whether it belongs to them. Change /orders/1041 to /orders/1042 and you see a stranger's order. The OWASP name for this is Insecure Direct Object Reference.

The fix is always the same: look up the record using both the ID and the current user (as in the example above), or let RLS enforce ownership in the database.

5. Admin flags and roles the user can edit

AI-generated schemas often put role or is_admin on the same profiles table that users are allowed to update. If the update policy covers the whole row, any user can promote themselves to admin with one request. Another variant reads a role from local storage or a query string, which the Next.js docs specifically call out as untrusted input.

Keep roles in a table users cannot write to, or restrict updates to specific columns, and verify the role on the server every time.

6. File storage that is more public than you think

Uploaded invoices, ID photos and contracts often end up in a public bucket because that made the images load during development. In Supabase, objects in a public bucket can be read by anyone who has the URL, with no policy needed. Supabase does block uploads until you add policies on storage.objects, so AI tools sometimes add a very broad policy to make uploads work.

Use private buckets for anything personal, generate signed URLs when a user needs to view a file, and scope storage policies to a folder per user.

7. No rate limits on expensive actions

Supabase Auth applies rate limits to its own sign-in, sign-up and email endpoints. Your custom functions are a different story. A contact form that sends email, an endpoint that calls an AI model, or a function that writes to the database can be hit thousands of times by a script. The result is a surprise bill, a blacklisted email domain or a flooded database. The Next.js data security guide recommends rate limiting exactly these kinds of operations.

Add per-IP and per-user limits to anything that costs money or sends messages, and set spending caps on your AI provider account.

8. Returning far more data than the screen needs

A select * that sends the full user record to the browser, including email, phone, internal notes or a password reset token, is a leak even if the UI only shows the name. Anything in a network response is visible to the user who received it.

Select only the columns the page needs. In Next.js, the docs recommend a server-only data layer that returns small, safe objects rather than raw database rows.

Laptop screen showing application source code
Photo: Arnold Francisca on Unsplash

Vibe coding best practices: a one-afternoon security checklist

You can work through this list yourself before paying anyone. Each line is a yes/no question.

CheckHow to test itRed flag
RLS on every tableCheck each table's RLS status in the Supabase dashboardAny exposed table with RLS off
Policies scoped to ownerRead each policy's using clausetrue, or no auth.uid() reference
No secrets in the bundleSearch the live site's JS in dev toolssb_secret_, service_role, any secret key
Server checks on every actionCall an endpoint with curl while logged outIt succeeds
Ownership checksChange an ID in the URL while logged inYou see another user's data
Roles protectedTry updating your own role column via the APIIt works
Private storageOpen a document URL in a private windowIt loads
Rate limitsSubmit a form 50 times quicklyEvery request succeeds
Minimal responsesInspect network responses on profile pagesFields the UI never shows
.env files out of GitCheck your repo historyA committed .env

What to do when you find a hole

  1. Rotate anything leaked first. A key that was ever in a public bundle or repository is compromised, even if you remove it now.
  2. Lock down the database. Enable RLS everywhere, then add the narrowest policies that keep the app working. Supabase's docs suggest testing allowed and denied operations for both the anon and authenticated roles.
  3. Move privileged calls to the server. Payments, emails, AI calls and admin operations belong in server routes or Edge Functions.
  4. Check your logs. Look for unusual reads or writes before the fix, in case you need to notify users.
  5. Stop the AI from undoing it. When you prompt for new features, tell the tool explicitly not to change policies or swap keys, and review every diff that touches auth or the database.

What this means for founders and app owners

An AI builder can get you to a working demo in days, and that is genuinely valuable. But "it works" and "it is safe to put real customer data in" are separate milestones. If you are about to take payments, store personal data, onboard a business client or raise money, a security review is part of shipping, not an extra.

If the checklist turned up red flags, or you are not sure how to read the results, a production audit gives you a prioritised list of what is exposed and how to fix it. If you would rather hand the whole thing over, I also take AI-built apps to production, keeping what works and rebuilding what does not.

Sources

About the author

Muhammad Moiz · Senior Full-Stack Engineer

I'm a senior full-stack engineer with 5+ years building production web apps in React, Next.js and Node.js. I build products end to end for founders and small businesses, and take over apps started with AI tools when they need to grow up.

More about me