FOR PEOPLE SHIPPING V0 APPS

What v0 apps get wrong

In round 2026-07 of the Vibe Security Index we scanned 25 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.
  • 25 repositories, drawn from a universe of 1,372. Every rate on this page is stated with the count behind it, because a percentage over 25 is a different kind of object than a percentage over 1,372: the finest distinction 25 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-07, generated July 27, 2026. Full method, sampling frame and limitations: Index methodology.

The shape of the sample

56%

had at least one critical finding in their own code

14 of 25

80%

had at least one high or worse

20 of 25

36

median findings per project, application code only

across 25 projects

94

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, Bolt and Base44 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 (175 occurrences across 22 of 25 projects), Console.log Left in Production Code (101 occurrences across 20 of 25 projects) and N+1 Query Pattern Detected (23 occurrences across 8 of 25 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 11 of 25 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-07. n = 25 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 AuthenticationVC003high44%11 of 2555
Unprotected Next.js API RouteVC065high44%11 of 2553
Potential Cross-Site Scripting (XSS)VC007high28%7 of 2539
API Route Without Request Body ValidationVC154medium40%10 of 2536
API Endpoint Without Rate LimitingVC008medium40%10 of 2513
Unsanitized Filename in File OperationsVC025medium24%6 of 2516
Timing-Unsafe Secret ComparisonVC043medium24%6 of 2514

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.

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 · 11 of 25 projects · 55 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.

highVC007 · 7 of 25 projects · 39 occurrences

Rendering HTML you did not write

Potential Cross-Site Scripting (XSS)

Why this workflow produces it

Read the two numbers in the header of this section together. It reached a minority of projects and fired several times inside each of them — the signature of a pattern that gets adopted once and then reused, rather than a one-off mistake.

v0 is a user-interface generator, which is exactly the context where this arrives. Ask for a markdown preview, a rich-text field, a templated email body, or anything that renders content someone else supplied, and the completion that works in a single step is the one that sets inner HTML directly.

The pattern is also already in the project before anyone asks for it: the component library v0 vendors contains it, and it shows up separately in the scaffold column further down this page. A shape that is already in the codebase is the shape the next completion imitates.

The change that prevents it

Sanitize at the boundary where untrusted content enters, not at the point where it is displayed, and prefer a renderer that returns React nodes over one that returns an HTML string. If you must set HTML directly, the value has to have been through a sanitizer on the same line.

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

Do not use dangerouslySetInnerHTML for content that came
from a user, an API, a database or a file. Render markdown
to React nodes rather than to an HTML string. If HTML must
be set directly, pass it through a sanitizer on the same
expression and never from a variable assigned earlier.

The scan that verifies it

Scan, then read each hit. This is a check where a human decides quickly and correctly, and the count should end at zero or at a short list of lines you have deliberately signed off.

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

Honest caveat. This check has a known false-positive mode of ours. One charting component in the vendored suite writes CSS custom properties into a style element from a module constant — that is theming, not an injection sink, and the rule that flags it is our bug. Some application-code hits are that same pattern copied out of the library, so read before you rewrite.

mediumVC154 · 10 of 25 projects · 36 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 · 10 of 25 projects · 13 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.

mediumVC043 · 6 of 25 projects · 14 occurrences

Comparing a secret with ===

Timing-Unsafe Secret Comparison

Why this workflow produces it

Verifying a webhook signature, checking an admin token against an environment variable, comparing an API key — the generated line reads perfectly naturally and is correct in every functional sense. The test passes, the webhook is accepted, the wrong token is rejected.

String comparison exits at the first byte that differs. How long the comparison takes therefore depends on how much of the prefix was right, and that is a signal an attacker can measure and climb one character at a time.

The change that prevents it

Compare secrets with a constant-time function on equal-length buffers, or better, use the verification helper your provider ships — most webhook SDKs have one, and it handles the replay window and the timestamp tolerance as well.

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

Never compare a secret, token, signature or hash with === or
!==. Use crypto.timingSafeEqual on buffers of equal length,
or the provider's own signature-verification helper where
one exists.

The scan that verifies it

Scan and confirm the count reaches zero. There is nothing to test by hand here — the fix is mechanical and the check is reliable on this pattern.

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

VC043 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.

Honest caveat. Being straight about the severity: exploiting a timing side channel across the public internet, through a serverless cold start, is genuinely hard. This is rated medium and it belongs there. Fix it because it is a one-line change, not because someone is doing it to you this week.

mediumVC025 · 6 of 25 projects · 16 occurrences

Filenames from the client used as paths

Unsanitized Filename in File Operations

Why this workflow produces it

Anything with an upload gets a handler that takes the file, reads the name the browser sent, and uses it to build a path. The browser is not a trustworthy source for that string, and a name is under the caller's complete control.

It fired on a smaller share of projects than the rest of this list, which is mostly a statement about how many of them have file handling at all rather than about how often the pattern is safe.

The change that prevents it

Never use a client-supplied filename as a path component. Generate your own identifier, store the original name as metadata if you want to show it back to the user, and validate the resolved path stays inside the directory you intended.

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

Never build a filesystem or storage path from a
client-supplied filename. Generate a server-side identifier
for the stored object and keep the original name as a
metadata field only. Validate the content type from the
bytes, not from the name or the declared MIME type.

The scan that verifies it

Scan, then check each hit resolves to a path you generated rather than one the caller named.

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

VC025 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.

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 25 projects (20 of 25) had a critical finding in that copied-in code — a higher rate than the 56% 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-07 at build time. Method and limitations: methodology.