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.