FOR PEOPLE SHIPPING V0 APPS

What v0 apps get wrong

In round 2026-09 of the Vibe Security Index we scanned 50 public repositories carrying v0's scaffold marker with all 212 rules. These are the security findings that repeated, why this particular workflow produces them, and the prompt change that prevents each one.

Read this first

  • This is not a claim that v0's product is insecure. We measured public repositories carrying its scaffold marker (v0-user-next config merge in the Next.js config), edited by humans for an unknown period afterwards. The marker proves the project started from the scaffold. It does not prove any particular line was generated rather than written, and attributing a finding to the builder is not something this design supports.
  • 50 repositories, drawn from a universe of 2,788. Every rate on this page is stated with the count behind it, because a percentage over 50 is a different kind of object than a percentage over 2,788: the finest distinction 50 repositories can draw is one repository. Differences of a few points here are noise.
  • No repository is ever named. Not here, not on the Index, not in the working data. Publishing the list would mean pointing at live vulnerabilities in other people's projects, so the results are aggregate only and the per-repository rows are anonymised by design.
  • Static analysis has false positives, including ours. A finding is something a person should look at, not a proven breach. Where a finding on this list is partly our own noise, it says so directly underneath.

Round 2026-09, generated September 3, 2026. Full method, sampling frame and limitations: Index methodology.

The shape of the sample

44%

had at least one critical finding in their own code

22 of 50

82%

had at least one high or worse

41 of 50

24.5

median findings per project, application code only

across 50 projects

104

median scanned files per project

size is a confound on every rate above

One absence, and what it is not. The three response-header checks — missing content-security policy, missing frame-ancestors control, missing content-type and referrer policies — are not in this builder's top ten at all, while for Lovable and Bolt in this same round at least one of them fired on every project scanned. We can tell you the absence is real and we cannot tell you it is good news: all three checks read an HTML entry file, and an App Router project usually has none in the repository, so there is nothing for them to look at. That is a limit of what we measured, not evidence that these projects send the headers. Only a response can tell you that — curl -sI https://your-app.example.

Why the loudest findings are not on this page

Sorted by raw occurrence count, the top of this builder's results is code quality, not security. Magic Numbers in Code (517 occurrences across 38 of 50 projects), Console.log Left in Production Code (354 occurrences across 37 of 50 projects), N+1 Query Pattern Detected (198 occurrences across 19 of 50 projects) and Synchronous File Operations (70 occurrences across 14 of 50 projects) are all info severity, a call this codebase made deliberately and has not changed. They are worth cleaning up. They are not what someone shipping an app to real users is asking about, and a security page that led with them would be counting lines instead of answering the question.

So the list below is filtered by severity and then ordered by severity, with how many projects a finding appeared in as the tiebreaker. Occurrence count never decides the order. What leads instead is API Route Missing Authentication, in 25 of 50 projects — a finding a project with no server surface in the repository cannot produce at all, and this scaffold ships one from the first prompt.

Security findings that repeat, application code only

From round 2026-09. n = 50 repositories. “Projects” is how many of them contained the finding at least once; occurrences is the total across all of them.

FindingSeverityProjectsOccurrences
API Route Missing AuthenticationVC003high50%25 of 50367
Unprotected Next.js API RouteVC065high48%24 of 50319
High-Entropy String Detected (Possible Secret)ENTROPYhigh28%14 of 50294
API Route Without Request Body ValidationVC154medium38%19 of 50175
API Endpoint Without Rate LimitingVC008medium38%19 of 5031
Stack Traces Exposed in API ResponsesVC037medium28%14 of 50103

This is the round's top-rules list with info and low severity removed. It is not the complete finding list for any project, and a check that never made this builder's top ten can still be the one that matters in yours. High-Entropy String Detected (Possible Secret), Stack Traces Exposed in API Responses appear in the table without a section below — real, but less specific to this workflow than the rest.

Each one, and what stops it coming back

Fixing the finding fixes one line. Changing the instruction that produced it fixes every line the next prompt writes, which is the only version of this that scales when the code is arriving faster than it can be read.

highVC003 · 25 of 50 projects · 367 occurrences

Route handlers that answer whoever asks

API Route Missing Authentication

Why this workflow produces it

A v0 project is a Next.js App Router app, so there is a real server in the repository from the first prompt — route handlers under app/api/, and server actions. That is a genuine advantage over a scaffold with nowhere to put a secret. It also means the project has an attack surface that a static single-page app does not have, which is why this is the finding that leads the page.

The two rules in the row above fired on the same number of projects, which is what you would expect: one is the general check for a handler with no authentication, the other is the Next-specific version of it. Read them as one finding seen from two angles rather than two separate problems.

An exported GET or POST in a route file is a live, publicly reachable HTTP endpoint the moment it deploys. Nothing in the framework requires a session before the body of the handler runs, and there is no default-deny step to forget — the default is answer.

Middleware does not close this by itself. It is opt-in and matched by path pattern, so a matcher written early does not know about a route generated twenty prompts later. Nothing breaks, nothing warns, and the app works perfectly for you because your own session is always present while you are testing. You never see the response the endpoint gives someone who is not signed in.

The change that prevents it

Put the rule in your project instructions rather than fixing handlers one at a time, and require the check inside the handler rather than only in a matcher. A matcher is a good second layer and a bad only layer.

Paste this into your project instructions, or into the prompt itself:

Every route handler under app/api/ resolves the caller's
session as its first statement and returns 401 before doing
any work if there is not one. Do this inside the handler,
not only in middleware — a middleware matcher does not cover
routes added later. If an endpoint is deliberately public,
put a comment saying why on the line above the export, and
keep it to GET with no user-specific data in the response.

The scan that verifies it

Scan and confirm the count drops. Then do the part a scanner cannot do for you: pick each remaining handler and request it with no cookie and no authorization header. What comes back is the answer.

npx xploitscan scan . -f json \
  | jq '[.findings[] | select(.rule == "VC003" or .rule == "VC065")] | length'

VC065 is outside the free rule set. The free scan runs 30 of the 212 rules, so a free plan never evaluates it. This command also counts VC003, which a free scan does run — so the number it returns on a free plan is the VC003 count alone, and it says nothing either way about VC065.

Honest caveat. Both checks read one file at a time, and that cuts in both directions. A route genuinely gated by a middleware matcher still reads as unprotected here, which is our false positive rather than your bug. A route whose gate is imported from a helper we cannot follow reads as unprotected too. Treat each hit as a handler worth opening, not as a confirmed hole.

mediumVC154 · 19 of 50 projects · 175 occurrences

Endpoints that accept whatever arrives

API Route Without Request Body Validation

Why this workflow produces it

A generated handler reads the request body and reaches straight into its fields. It works flawlessly, because the only thing that has ever called it is the form on the other side of the same prompt, sending exactly the shape the handler expects.

The endpoint does not require that shape, though. It accepts anything. Extra fields ride through into an ORM call, a missing field becomes undefined three layers down, and a string where a number was expected becomes a runtime error whose text is often returned to whoever sent the request.

This lands harder in a generated codebase than a hand-written one, because the client and the handler are usually produced in the same breath. They agree by construction, so the contract between them never gets written down anywhere and nothing enforces it at the boundary.

The change that prevents it

Ask for a schema at the top of every handler, once, and it applies to every route generated afterwards. Parse, do not merely type-assert — a TypeScript interface is erased at build time and validates nothing at runtime.

Paste this into your project instructions, or into the prompt itself:

Every route handler validates its request body with a zod
schema as the first thing it does after resolving the
session, and returns 400 with a generic message when parsing
fails. Use the parsed result, never the raw body. Do not use
a TypeScript type assertion as validation.

The scan that verifies it

Scan, then send a request with an unexpected field and a wrong-typed field. A 400 is the right answer; a 500 with a stack in it is two findings, not one.

npx xploitscan scan . -f json \
  | jq '[.findings[] | select(.rule == "VC154")] | length'

VC154 is outside the free rule set. The free scan runs 30 of the 212 rules, so a free plan never evaluates it. On a free plan this command therefore returns 0 whether or not the finding is in your code — a zero from it is not a clean result.

mediumVC008 · 19 of 50 projects · 31 occurrences

Nothing standing between your endpoint and a loop

API Endpoint Without Rate Limiting

Why this workflow produces it

It spread thinly — a couple of endpoints per project rather than the whole surface — which is what makes it easy to miss. There is no moment where the app degrades to warn you, because route handlers deploy as serverless functions and those scale up quietly and automatically.

v0 projects very often include a handler that calls a model provider, because that is a large share of what people build with it. An endpoint that costs you money per request, with no session required and no limit applied, is a different kind of exposure from the rest of this page: the bill is the alarm, and it arrives at the end of the month.

The change that prevents it

Put a limit in front of anything that costs money or sends mail, and key it on something better than an IP address wherever you have a session to key it on. The cheapest version — a limit at your hosting platform's edge — takes minutes and covers most of the risk.

Paste this into your project instructions, or into the prompt itself:

Any route handler that calls a paid API, sends email, or
writes to storage must be rate limited. Apply the limit
before the expensive call, key it on the user id when the
route is authenticated and on the client IP when it is not,
and return 429 when the limit is hit.

The scan that verifies it

Scan, then confirm the limit actually engages: call the endpoint in a tight loop from one client and check you get a 429 rather than a bill.

npx xploitscan scan . -f json \
  | jq '[.findings[] | select(.rule == "VC008")] | length'

Honest caveat. This check looks for a rate-limiting mechanism it recognises in the file. A limit enforced at your CDN, your API gateway or your WAF is invisible to it and will still be reported. That is a real limitation and a fair reason to mark a finding reviewed.

Where our detection stands on this code, stated plainly

A separate research pass over the same sampling frame went looking for request handlers with no authentication check the analysis could find. On v0-shaped code this is the one place we can report a genuinely good number — and the reason is worth knowing, because it tells you where our detection is likely to be weaker.

124

handlers in the v0 slice where the analysis could find no authentication check

86%

of those 124 that our shipped rules flag today — a point estimate, not the middle of the interval below

95% confidence interval 79%–91%

Read that as a statement about our rule set rather than about the code. v0 generates Next.js route handlers, and Next.js route handlers are exactly the shape our route rules model, so a high catch rate here says more about the match between a framework and a rule set than it does about anyone's security. Point the same rules at a framework we do not model and the number would be far worse.

This is not a builder ranking. The research pass behind these figures states in its own limitations that it does not support comparing one builder against another, so this page reports its own slice and nothing relative to anyone else's.

For context from the same pass, across all four builders and 174 repositories: 51% [44%–58%] had any server-side handler at all, and of those, 69% [58%–77%] had at least one handler with no authentication check the analysis could find. Those two figures are round-level, not v0-specific, and are labelled that way on purpose — the research pass does not break them out per builder, so neither does this page.

What 86% does not mean

  • It is not a pass rate for your app. 124 handlers with no visible auth check were found in this slice in the first place. Catching most of them is the scanner working; it is not evidence that the code was fine.
  • A shape is not a vulnerability. Nothing was exploited and no request was ever sent to a running application. A handler in this class is one where a static pass could not find an auth check. Some fraction are protected by means the method cannot see.
  • 47.5% of handlers were undecidable from a single file and were classified as such rather than guessed at. A handler goes to that bucket whenever protection might exist somewhere the analysis cannot look — middleware mounted before the route table, for instance. Every rate above excludes them, so 86% describes the decidable slice, not the whole corpus.
  • Handler counts are concentrated. A handful of large repositories contribute most of the handlers, so a handler-level percentage is closer to a statement about those projects than about a typical one.

We publish this because a scanner that only advertises what it catches is not much use for deciding whether to trust it, and that has to include the rounds where the number flatters us. Our benchmark page carries the same disclosure in the other direction — a held-out access-control case that no scanner we test catches, including ours.

The findings we do not count against you

Every number above covers application code only. A v0 project also arrives with a copied-in component library, and findings there are separated before any headline figure is computed. They are reported, never hidden, and they never touch a grade.

In this round 80% of the 50 projects (40 of 50) had a critical finding in that copied-in code — a higher rate than the 44% in their own code. That column is mostly our false positive. One component in the suite sets CSS variables through a raw-HTML style block, which trips a critical cross-site-scripting rule. It is CSS-variable theming, not an injection sink, and a rule that flags it is our bug rather than yours or the builder's.

Because that one file is byte-identical everywhere it is copied, it shows up in almost every project that vendors the suite. Reading it as a security signal would mean ranking projects by how much of a component library they happened to copy in. This is also why your grade is computed on your own code by default — you can opt into the stricter view with xploitscan scan . --grade-vendored if you want everything counted.

Find out which of these are in your project

Same application-versus-scaffold split, run against your repository instead of someone else's. No credit card and no signup for the first scan — that one runs the 30 free rules rather than the 212 this round used, and the full set comes with a paid plan.

Numbers on this page are read from the published Vibe Security Index round 2026-09 at build time. Method and limitations: methodology.