Skip to main content

Vibe coding security: the bugs AI-built apps ship with

· 9 min read
Vulkro
Security research

Vibe coding security is not a new kind of security. The apps that come out of AI app builders and AI coding agents fail in the same ways hand-written apps do. What changes is the rate. An app that would have taken a team a month now takes an afternoon, and nobody on that afternoon was thinking about who else can call the endpoints.

The bugs are also surprisingly consistent. Pages that look locked but are not. A database queried straight from the browser with nothing on the server side deciding who sees which row. A secret key in the JavaScript bundle. A token that is read but never verified. A record fetched by id with no check on whose it is.

This post goes through those five, with the research behind them, then gives a pre-ship checklist and the commands to check an app before it goes live.

Is AI-generated code less secure?​

The research says it is at least not more secure, and that the people using it tend to think it is. Two peer-reviewed studies are worth knowing:

  • Asleep at the Keyboard? (Pearce et al., IEEE Symposium on Security and Privacy 2022) prompted an AI code assistant with 89 security-relevant scenarios and found about 40% of the 1,689 programs it produced were vulnerable.
  • Do Users Write More Insecure Code with AI Assistants? (Perry et al., ACM CCS 2023) ran a user study and found participants with an AI assistant "wrote significantly less secure code than those without access", and were more likely to believe their code was secure.

The models have improved since. The second finding has not obviously changed: when code arrives fast and works, people review it less.

The five bugs vibe-coded apps keep shipping​

1. Routes and server actions with no auth check​

The UI hides the admin page from logged-out users, so it feels protected. The API route or server action behind it checks nothing:

'use server';
export async function deleteProject(projectId: string) {
await db.project.delete({ where: { id: projectId } }); // anyone can POST this
}

A Next.js server action is a public HTTP endpoint, whether or not any page links to it. The check belongs inside the action. We go through the details in Next.js server actions are public endpoints.

2. Client-side database access with no row level security​

App builders love Supabase and Firebase because the browser can talk to the database directly. That is only safe if the database itself enforces who can read which rows. In Supabase that is Row Level Security, in Firebase it is security rules.

CVE-2025-48757 is the clearest public example. NVD records an insufficient Row Level Security policy in sites generated by the Lovable app builder that let "remote unauthenticated attackers" read or write arbitrary tables. The researcher's write-up describes the generated apps querying the database from the browser with the public anon key and relying on policies that were missing or too loose. The supplier disputes the CVE, on the basis that each customer is responsible for securing their own application's data. Either way, the pattern is the lesson:

-- Without this, the anon key in your bundle can read every row
alter table invoices enable row level security;

create policy "owners read their invoices" on invoices
for select using (auth.uid() = owner_id);

The Firebase equivalent is a rules file that says allow read, write: if true, usually left over from a quick-start.

3. Secrets in the frontend bundle​

Frameworks make it easy to ship a variable to the browser on purpose. Next.js inlines any NEXT_PUBLIC_ variable into the client JavaScript, and Vite exposes VITE_ variables the same way. An AI coding agent that hits "undefined in the browser" will often fix it by adding the prefix. If the variable was a payment secret key or the Supabase service_role key, it is now readable by anyone who opens developer tools. The service_role key bypasses Row Level Security entirely.

The rule is simple: anything the browser can read is public. Secret keys stay on the server, behind a route that checks the caller.

4. Tokens that are decoded, not verified​

const user = jwt.decode(token); // reads claims, checks nothing
if (user?.role === 'admin') { ... }

decode parses a token anyone can write by hand. verify, with your key and a pinned algorithm, is the only call that makes the claims worth believing. One wrong function name turns every protected endpoint into a public one. The full story is in the sign-in that never checks the signature.

5. Records fetched by id with no ownership check​

export async function GET(req: Request, { params }: { params: { id: string } }) {
const session = await auth();
if (!session) return new Response(null, { status: 401 });
return Response.json(await db.invoice.findUnique({ where: { id: params.id } }));
}

The route checks that someone is logged in, then returns any invoice to anyone who asks. Add ownerId: session.user.id to the query. This is the bug at the top of every API security list, covered in broken access control is still number one.

A sixth risk sits before any of this: packages that do not exist. AI assistants sometimes suggest dependency names that were never published, and attackers register them.

Vibe coding security checklist before you ship​

  1. Every API route and server action checks the session itself. Hidden in the UI is not protected.
  2. Every Supabase table the browser can reach has Row Level Security enabled and a policy per operation. No Firebase rule says if true.
  3. No secret key has a NEXT_PUBLIC_, VITE_ or PUBLIC_ prefix. The service_role key is never in client code.
  4. Tokens are verified with a key and a fixed algorithm, never just decoded.
  5. Every lookup by id includes the owner or tenant in the query.
  6. Every dependency the assistant added is a real, established package.
  7. You have tried the app as a second user, and as nobody, and both were refused.

How to check a vibe-coded app with Vulkro​

Vulkro reads the repository on your machine. It uses no AI to decide findings, so the same code gets the same answer every run. Run a scan on the project:

vulkro scan .

These are the checks that map to the list above:

  • nextjs-server-action-no-authz for a server action that reads or writes by its own arguments with no authorization, and route-level checks for API routes with no guard.
  • SUPA-RLS-003 for browser code querying Supabase tables when no migration ever enables Row Level Security, SUPA-RLS-002 for a service_role key reaching browser code, including through a NEXT_PUBLIC_ or VITE_ name, and SUPA-RLS-001 for Firebase rules open to anyone.
  • Hardcoded secrets in source, config and .env files, with known provider formats (a Stripe live key, an AWS access key) raised to High. Vulkro reads the repository, not the built bundle, so a key injected at build time from your hosting dashboard is not visible to it.
  • forgeable-token-auth for sign-in that decodes a token without verifying it, reported as Critical with the number of endpoints it guards.
  • idor-authn-no-ownership for authenticated lookups by a caller-supplied id. These are marked Not checked, because whether a record should be shared is sometimes a business rule.

One limit worth stating: SUPA-RLS-003 looks for enable row level security in the repository's Supabase migrations. If your policies were created by clicking in the dashboard, they are not in the repository, and the check cannot see them. Export your schema into migrations so the policies are reviewable at all.

Before installing what the assistant suggested, vulkro slopcheck --manifest . checks the dependency list for known-malicious packages, documented AI hallucinations and typosquats, offline.

Let the AI coding agent check its own work​

The best time to catch these bugs is while the agent is writing them. vulkro mcp serve runs Vulkro as an MCP server, so Claude Code, Cursor, Windsurf and other MCP clients can call scan_project, scan_file and explain on the code they just wrote. vulkro guard install goes further for Claude Code, Cursor and Windsurf: it wires a hook that scans every file the agent writes and feeds findings back before the agent moves on. The skill for AI coding agents tells the agent when to call it. The broader setup is in securing AI coding agents.

Frequently asked questions​

Is vibe coding safe for production apps?​

It can be, if someone checks the parts AI-built apps most often get wrong: authorization on every route, database access rules, secrets in client code, token verification and ownership checks. Speed of writing is not the risk. Shipping without that review is.

Is the Supabase anon key a secret?​

No. The anon key is designed to be public and ships in the browser. It is safe only when Row Level Security is enabled with correct policies on every table it can reach. The service_role key is the secret one, and it must never reach client code.

Can AI coding agents write secure code?​

They can, especially when asked to and when given a way to check their output. Studies have found that AI-assisted code is often vulnerable and that developers trust it too much, so an independent check on each change matters more than the prompt.

What is the fastest security check before launching an AI-built app?​

Sign in as a second user and try to open the first user's records by changing an id, then try every API route while logged out. Then run a scan over the repository to find the routes, rules and keys you did not think to try.

Where to see it​

The AI coding agents use case shows how an assistant asks Vulkro for findings instead of reading the whole codebase, and Vulkro for AI coding assistants covers the connections. The docs have the MCP server, the guard and slopcheck. To check your app before launch, start here.