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.
- Published
- min read
- 9 min read
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
publicschema. - 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.
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.
| Check | How to test it | Red flag |
|---|---|---|
| RLS on every table | Check each table's RLS status in the Supabase dashboard | Any exposed table with RLS off |
| Policies scoped to owner | Read each policy's using clause | true, or no auth.uid() reference |
| No secrets in the bundle | Search the live site's JS in dev tools | sb_secret_, service_role, any secret key |
| Server checks on every action | Call an endpoint with curl while logged out | It succeeds |
| Ownership checks | Change an ID in the URL while logged in | You see another user's data |
| Roles protected | Try updating your own role column via the API | It works |
| Private storage | Open a document URL in a private window | It loads |
| Rate limits | Submit a form 50 times quickly | Every request succeeds |
| Minimal responses | Inspect network responses on profile pages | Fields the UI never shows |
| .env files out of Git | Check your repo history | A committed .env |
What to do when you find a hole
- Rotate anything leaked first. A key that was ever in a public bundle or repository is compromised, even if you remove it now.
- 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.
- Move privileged calls to the server. Payments, emails, AI calls and admin operations belong in server routes or Edge Functions.
- Check your logs. Look for unusual reads or writes before the fix, in case you need to notify users.
- 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
- CVE-2025-48757 record (CVE Program, cvelistV5 on GitHub) (opens in a new tab)
- Supabase docs: Understanding API keys (opens in a new tab)
- Supabase docs: Row Level Security (opens in a new tab)
- Supabase docs: Storage access control (opens in a new tab)
- Supabase docs: Auth rate limits (opens in a new tab)
- Next.js docs: Environment variables (opens in a new tab)
- Next.js docs: Data security (opens in a new tab)
- OWASP Top 10 (opens in a new tab)
- Cover photo: FlyD on Unsplash

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