Vibe Coding Security: The Mistakes That Leak Data
Seven security issues turn up again and again in apps built by prompting: secrets in the browser, open API routes, tables without row-level security, loose CORS, trusted client input, invented packages and chatty errors. Each one, where it comes from, and the fix.
8 min read
The vibe coding security issues that leak data are few, and they repeat: a secret key shipped to the browser or committed to the repository; an API route that never checks who is calling; database tables with no row-level security; CORS that lets any site read your responses; server code that trusts whatever the client sends; dependencies that are unpinned or do not exist; and error pages that print stack traces. An AI app builder or coding agent can produce every one of these while the app looks finished, because each one works perfectly in a demo. Check for all seven before anyone outside the team signs in.
This is about the app you build, not the agent that builds it. What vibe coding is and how to keep it under control is in what is vibe coding. Locking down the agent itself, its shell, network and tokens, is in security controls for AI coding agents. Neither is repeated here.
Why generated apps fail in the same places
A prompt describes what the app should do for the person using it. Security is mostly about what the app must refuse to do for someone else, and nobody types that in. So the model builds the happy path: the page loads, the data appears, the form saves. The checks that only matter when a stranger calls your API directly, with someone else’s ID in the request, are exactly the ones a working demo never exercises. Treat every item below as a question to ask of the code, not an accusation of any tool.
1. Secrets in client code or the repository
Anything the browser receives, anyone can read. Next.js documents that a variable prefixed NEXT_PUBLIC_ is inlined into the JavaScript sent to the browser at build time, and other frameworks have their own public prefix. A generated app that needs a key on the client will often reach for that prefix, and a secret key then ships in every page load.
Backends make the stakes plain. Supabase’s API keys guide (opens in a new tab) says a secret key bypasses every row-level security policy you have, and should never go in a browser, a shipped application or source control. Only the publishable key belongs on the client.
- Fix: keep secret keys in server-only environment variables, used in route handlers or edge functions, never behind a public prefix.
- Fix: search the built output, not just the source, for key prefixes before launch. A key that reached the bundle or a commit is leaked: rotate it, then remove it.
- Fix: keep
.envfiles out of git and turn on secret scanning with push protection in your code host, so the next accidental commit is blocked.
2. API routes with no authorisation check
The page hides the admin button, so the app looks protected. The route behind it answers anyone. The OWASP Authorization Cheat Sheet (opens in a new tab) is blunt about both halves: validate permissions on every request, deny by default, and never rely on client-side checks. It also covers the quieter version, where a signed-in user changes an ID in the URL and reads another customer’s record.
export async function GET(req: Request, { params }) {
const user = await getSessionUser(req); // who is calling?
if (!user) return new Response(null, { status: 401 });
const { id } = await params;
const invoice = await db.invoice.findFirst({
where: { id, ownerId: user.id }, // is it theirs?
});
if (!invoice) return new Response(null, { status: 404 });
return Response.json(invoice);
}Fix: put the check in the route or in shared middleware, scope every query to the caller, and test each route with no session and with a second user’s session. A 200 for either is the bug.
3. Tables without row-level security
Backends such as Supabase let the browser query the database directly with the publishable key, which is only safe when the database itself decides which rows each user may see. Supabase’s row-level security guide (opens in a new tab) says a table in an exposed schema without RLS is readable and writable by any role with a grant on it, and tells you to enable RLS on every such table. With RLS on and no policies, the publishable key can read nothing, which is the safe failure.
alter table public.profiles enable row level security; create policy "Users can view their own profile" on public.profiles for select to authenticated using ( (select auth.uid()) = user_id );
Fix: list every table in the exposed schema and confirm each has RLS enabled and a policy per operation you intend to allow. Then sign in as two different users and try to read and update each other’s rows.
4. CORS that allows every origin
When a cross-origin request fails in development, the quickest fix a model can offer is Access-Control-Allow-Origin: *, or code that copies the request’s Origin header straight back. OWASP’s HTML5 Security Cheat Sheet (opens in a new tab) says to allow only selected, trusted domains, and specifically not to use the wildcard or blindly return the Origin header. It adds that the header is not an access control on its own.
Fix: an explicit list of your own origins, set per environment, applied only to the routes that need cross-origin access. Everything else keeps the browser’s default of same-origin only.
5. Trusting what the client sends
Generated forms validate nicely in the browser, and the server often takes the posted object as it arrives: price, role, isAdmin and all. Anyone with the browser’s developer tools can send a different body. OWASP’s input validation guidance says validation must happen on the server before data is processed, because client-side checks can be bypassed, and that an allowlist of what is expected beats a list of what is forbidden.
- Fix: validate every request body on the server against a schema, and reject fields you did not expect instead of saving them.
- Fix: compute anything that matters on the server. The client sends a product ID and a quantity; the server looks up the price.
- Fix: never let a request body set its own role, owner or account.
6. Unpinned and invented dependencies
Models sometimes recommend packages that do not exist. A study presented at USENIX Security 2025, We Have a Package for You! (opens in a new tab), generated 576,000 code samples with 16 models and found hallucinated package names in at least 5.2% of recommendations from commercial models and 21.7% from open-source ones, over 205,000 unique names. An attacker who registers one of those names gets installed by the next person who runs the suggested command, which is what people now call slopsquatting.
- Fix: before installing anything an assistant suggests, check the package exists, is the one you meant, and has a real history and maintainer.
- Fix: commit the lockfile and install from it in CI. npm documents that
npm cirequires a lockfile, fails if it disagrees withpackage.json, and never rewrites either. - Fix: turn on dependency alerts in your code host so known vulnerabilities in what you did install are reported.
7. Errors that explain too much
A stack trace in a production response tells an attacker your framework, versions, file paths and sometimes a query. OWASP’s Error Handling Cheat Sheet (opens in a new tab) sets the rule: return a generic response to the client and log the details on the server for investigation. The 2025 OWASP Top 10 gives the subject its own category, Mishandling of Exceptional Conditions.
Fix: a single error handler that returns a short message and a reference ID, with the full error logged against that ID. Check that debug mode is off in the production build, and that database errors never reach the response body.
A pre-launch checklist to copy
SECRETS [ ] No secret key behind a public prefix (NEXT_PUBLIC_, VITE_ and the like) [ ] Built bundle searched for key prefixes; nothing found [ ] .env files ignored by git; secret scanning and push protection on [ ] Any key that was ever exposed has been rotated ACCESS [ ] Every API route checks the session and ownership on the server [ ] Each route tested with no session and with a second user's session [ ] RLS enabled on every table in the exposed schema, with policies [ ] Two test users cannot read or change each other's rows BOUNDARIES [ ] CORS allows a named list of origins, not * or a reflected Origin [ ] Every request body validated on the server; unknown fields rejected [ ] Prices, roles and owners computed on the server, never taken from the client SUPPLY CHAIN [ ] Every dependency checked to exist and be the intended package [ ] Lockfile committed; CI installs with npm ci (or your manager's equivalent) [ ] Dependency alerts switched on ERRORS [ ] Production returns generic errors with a reference ID [ ] Full errors logged server-side; debug mode off
Run it yourself rather than asking the same assistant whether its code passes. A second person, or a second tool with a security focus, will find what the first missed; the kinds of reviewer are compared in AI code review tools.
Turn each finding into a task
A checklist ticked in a chat window is forgotten by Friday. On a fenbs board, each failed item becomes a bug in To Do with a priority from 1 to 10, where 1 is most urgent: an exposed secret key is a 1, a verbose error page perhaps a 4. The note says where the problem is and how you found it. When it is fixed, the task’s test status says how it was checked, such as Tested with a note like “second user gets 404 on /api/invoices/12”, or Needs owner check when only a person can confirm it on the live site.
Some findings are accepted rather than fixed, such as a wildcard CORS header on a genuinely public, read-only endpoint. Close those as Won’t fix with the reason, and record the choice on the Decisions page, where the decider is always a person. An AI assistant connected to the board can file and update these bugs, and History shows each change under its name.
Related
Keeping the assistant itself contained: AI agent security best practices. The risks mapped to OWASP’s agent guidance: OWASP AI agent security. A board ready for findings: the bug tracker template.