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.
criticalVC148 · 2 of 12 projects · 7 occurrences
Secrets travelling back out in an error
Secret Leaked in Error Response
Why this workflow produces it
Start with the reach, because it is the whole context for this section: it appeared in a small number of the projects scanned, and the header above this paragraph gives the exact count. This page does not convert that into a percentage, and neither should you.
It leads this page on severity alone. A critical finding in a couple of projects is still the most serious thing this round found in this slice, and pushing it below a finding that reached every project would be sorting by comfort rather than by risk.
What the check matches is a server error response carrying a secret — or a variable named like one — back to whoever triggered the error. That shape turns up in the least convenient place imaginable here. Most projects with this scaffold are client-only, so when one does acquire a server file it is usually there for exactly one reason: to hold a key the browser must not see and to proxy the calls that need it.
That file then gets the treatment every new integration gets while you are trying to make it work. An error path that echoes the configuration back is genuinely useful when the upstream call keeps failing and you cannot see why — and there is no later moment where the app misbehaves to remind you it is still in there.
The change that prevents it
Draw a hard line at the response boundary: configuration goes to your logs, never into a response body. If you needed to see a value to debug something, what you needed was a log line, not a field in the JSON you send back.
Paste this into your project instructions, or into the prompt itself:
Never put an environment variable, a config object, an API
key, or any variable whose name contains key, token, secret
or password into an HTTP response body — including error
responses, and including diagnostic fields that only report
whether a value is set. Errors return a generic message and
a random correlation id. Log the detail server-side against
that same id.
The scan that verifies it
Scan, then force the failure on purpose: point the integration at a bad credential locally and read the entire response, headers included. That is the view someone else gets.
npx xploitscan scan . -f json \
| jq '[.findings[] | select(.rule == "VC148")] | length'
VC148 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. A secret-named variable in a response is not proof that a live credential was published. This check matches names and shapes, so some hits are a boolean saying whether a value is configured, or a redacted placeholder. It is rated critical because of the cost of being wrong in the other direction — the value in question is somebody's production key. Read every one of them; there are few enough that it will take minutes.
highVC003 · 3 of 12 projects · 45 occurrences
Server routes that answer whoever asks
API Route Missing Authentication
Why this workflow produces it
Read the reach and the occurrence count in the header above together, because they point in opposite directions. Few projects, many occurrences. The projects that have a server surface at all have a lot of unprotected handlers each, rather than many projects having one apiece.
The reason so few projects appear here is not a security property and should not be read as one. Most projects with this scaffold are client-only single-page apps — this is the smallest median project in the round by a wide margin — so most of them simply have no server on which to get this wrong.
Where a server does show up, it usually arrives late: the app already works, something needs to happen off the client, and a handler gets added to make that one thing possible. It inherits no conventions, because there were none to inherit. Nothing in the project establishes that a handler should check who is calling before it does the work.
And it never announces itself, because your own session is always present while you are testing. You never see the response the endpoint gives to somebody who is not signed in.
The change that prevents it
Write the rule down once, in your project instructions, so it applies to every handler generated after it rather than being remembered per handler.
Paste this into your project instructions, or into the prompt itself:
Every server route resolves the caller's identity as its
first statement and returns 401 before doing any work if
there is not one. Do this inside the handler itself, not
only in a middleware matcher — a matcher does not cover
routes added later. If an endpoint is deliberately public,
put a comment above it saying why.
The scan that verifies it
Scan and confirm the count drops. Then do the part a scanner cannot do for you: call each remaining handler with no cookie and no authorization header, and read what comes back.
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 of these checks read one file at a time, which cuts both ways. A route genuinely gated by middleware still reads as unprotected — our false positive, not your bug — and a route whose gate is imported from a helper we cannot follow reads the same way. Treat each hit as a handler worth opening rather than a confirmed hole.
mediumVC037 · 3 of 12 projects · 8 occurrences
Error handlers that hand back the internals
Stack Traces Exposed in API Responses
Why this workflow produces it
The generated catch block is almost always the same three lines: catch the error, put its message in the body, return a 500. It is a reasonable thing to write while building, nothing misbehaves afterwards, and so it never comes up again.
What ships to the caller is your ORM's error text, your table and column names, and often the query that failed — a free map of your schema for anyone who sends a deliberately malformed request and reads the reply.
The change that prevents it
Ask for the error contract explicitly, once, and it applies to every handler generated afterwards. Log the real error server-side, return a generic message and a correlation id to the caller.
Paste this into your project instructions, or into the prompt itself:
Every server error response returns a generic message and a
random correlation id, never error.message and never a
stack. Log the full error with that same id server-side so
it can still be traced. Apply this to every handler you
generate.
The scan that verifies it
Scan, then confirm by hand: send a request that is guaranteed to fail and read the response body. It should tell you nothing you did not already know about your database.
npx xploitscan scan . -f json \
| jq '[.findings[] | select(.rule == "VC037")] | length'
highVC020 · 12 of 12 projects · 22 occurrences
The response headers a static host does not set for you
Missing Content Security Policy (CSP)
Why this workflow produces it
This is really three findings in the table above — the missing content-security policy, the missing frame-ancestors control, and the missing content-type and referrer policies. They travel together because they have one cause.
This scaffold builds to a static bundle served by a host. All of these are response headers, and a static file does not carry a response header. Something in front of it has to add them, and the default deployment target usually adds none.
Nothing looks broken as a result, because none of these headers change how a working app behaves. They only change what happens during an attack, which is why they survive every round of testing you do before launch.
The change that prevents it
Add them where the host reads them: a _headers file on Netlify or Cloudflare Pages, the headers array in vercel.json on Vercel. The content-security policy is the one worth real time and the one most likely to break something, so run it in report-only mode first and read the reports before you enforce it. The rest are close to free.
The scan that verifies it
Scan for all three, then check the live response and confirm the headers actually arrive: curl -sI https://your-app.example. The scan reads your repository; only the response proves a header is really being sent.
npx xploitscan scan . -f json \
| jq '[.findings[] | select(.rule == "VC020" or .rule == "VC027" or .rule == "VC056")] | length'
VC027 and VC056 are outside the free rule set. The free scan runs 30 of the 212 rules, so a free plan never evaluates them. This command also counts VC020, which a free scan does run — so the number it returns on a free plan is the VC020 count alone, and it says nothing either way about VC027 and VC056.
Honest caveat. This cluster fired on every project in the sample, which says more about the scaffold than about any developer — and part of it is a measurement limit on our side. The scanner reads your repository, so a host that already injects these headers still reads as missing. Check the live response before you spend an afternoon on it.