What the testing actually found
That headline figure comes from
Veracode's spring 2026 GenAI code security update, published in March, which has been running the same experiment on language models for a couple of years now. More than 150 models, a fixed set of security-sensitive coding tasks.
Syntax correctness came in above 95%. Security pass rate was 55%.So the code compiles, and roughly half of it has a known vulnerability in it.
The breakdown by language is worth sitting with.
Python managed 62%, C# 58%, JavaScript 57%. Java came last at 29%, which is a grim result for a language that runs a large share of the world's banking software.
The split by vulnerability class is stranger, and more useful:
| SECURITY PASS RATE BY WEAKNESS — VERACODE, MARCH 2026 |
|---|
| Weakness | CWE | Pass rate |
|---|
| Insecure cryptographic algorithm | CWE-327 | 86% |
| SQL injection | CWE-89 | 82% |
| Cross-site scripting | CWE-80 | 15% |
| Log injection | CWE-117 | 13% |
Models are good at the famous ones. SQL injection has twenty years of blog posts, conference talks and Stack Overflow answers behind it, and parameterised queries show up in every tutorial written since about 2010. That material is in the training data, heavily, and it shows.
Output encoding does not get the same treatment. Log injection gets a footnote at best. The result is a model that will reliably protect you from the attack everyone already knows about and reliably hand you the two that quietly matter more in a modern web app.
The other finding is the one that should shape planning.
Veracode's results have been essentially flat since their 2025 round, across model generations and despite a great deal of marketing to the contrary. Reasoning-focused models did a bit better at 70 to 72%. Better is not the same as good, and waiting for the next release to fix this has not worked so far.
Working and safe are different tests
That 95% syntax figure against a 55% security figure is the whole problem in two numbers.
When you ask an agent for a login form, you get a login form. It renders, it posts, it sets a session, the happy path works and your tests go green. Your tests go green because you wrote them against the thing you asked for. Nobody writes a test for the request they never made.
The question that catches you out is not whether the form works. It is whether it also accepts a session token belonging to somebody else, whether the password reset flow leaks whether an account exists, whether the error message hands back a stack trace with your connection string in it. Those are not bugs in the sense of something failing. Everything works exactly as specified. The specification was just incomplete, and the model had no reason to say so.
Apiiro looked at this inside Fortune 50 engineering teams and found
AI-assisted developers committing code at three to four times the rate of their colleagues, and introducing security findings at ten times the rate. Privilege escalation issues specifically
went up 322%. The same
Cloud Security Alliance note tracks Georgia Tech's Vibe Security Radar, which is trying to count the fallout in public CVEs:
6 in January 2026, 15 in February, 35 in March. The researchers think the real figure across open source is five to ten times what they can confirm.
None of this makes AI-written code uniquely cursed. Humans have been shipping cross-site scripting since the nineties. What has changed is throughput. The same review capacity now sits in front of three or four times the volume, and the things that get skipped are the things that were never easy to spot.
The dependency you never chose
Which brings us back to that Tuesday evening, and to a worm most people call
ChainDrop.
The entry point was not npm.
Attackers got hold of GitHub credentials belonging to the maintainer of keyv, cacheable and ecto, pushed poisoned commits, and then let the project's own release automation do the publishing. That detail matters more than the malware does. The packages went out through
npm's OIDC trusted publishing with valid SLSA provenance attestations, which is the mechanism the ecosystem spent years building specifically to stop this sort of thing. Provenance tells you a package came from the repository it claims to come from. It has nothing to say about whether that repository was compromised twenty minutes ago.
From there it was a
preinstall hook running
node setup.mjs, which
downloaded the genuine Bun runtime from GitHub's own release servers and used it to execute a 727KB obfuscated payload. Every network request during the interesting part pointed at github.com, so anything watching for suspicious domains saw nothing worth flagging.
What it took is a long list, and one entry on it should give anyone using coding agents pause:
- npm and GitHub tokens, GitHub CLI credentials, SSH keys
- AWS credentials via IMDSv2, Secrets Manager and SSM, swept across 16 regions
- Kubernetes and HashiCorp Vault configuration
- GitHub Actions runner memory, scraped by dumping process memory with sudo
- .claude/, .cursor/, .openai/ and .anthropic/ configuration directories
That last line is new, and it is a sensible thing for an attacker to want. Those directories hold API keys, and increasingly they hold project instructions and context files describing your architecture in plain English. Whoever wrote this understood that a developer's AI tooling is now a credential store and a documentation cache at the same time.
Here is our own dependency tree, unedited:
$ npm ls keyv flat-cache file-entry-cache
itsybytesywebsite@0.1.0
`-- eslint@9.39.2
`-- file-entry-cache@8.0.0
`-- flat-cache@4.0.1
`-- keyv@4.5.4
We did not install keyv. We installed a linter. The linter wanted a cache, the cache wanted a store, and four hops later a package with 150 million weekly downloads is in our build, maintained by someone we have never heard of, and for one evening in August it was the most dangerous thing on the registry.
And there is a distinctly modern way to walk into it.
Around 20% of AI-generated code samples reference packages that do not exist. Attackers have worked out that they can watch for the names models hallucinate most often, register them, and wait. It has a name now. Slopsquatting.
Set a minimum release age today. It takes five minutes.
This is the single highest-value change most teams can make this week, and almost nobody has it switched on.
Look at
the ChainDrop timeline, converted to Brisbane time:
| CHAINDROP — TUESDAY 4 AUGUST 2026 (AEST) |
|---|
| Time | What happened |
|---|
| 7:35pm | keyv@6.0.0 published with a valid provenance attestation |
| 7:38pm | Automated second wave begins republishing into other packages |
| 8:39pm | npm starts unpublishing |
| 11:20pm | Second wave ends at 444 packages, 2,212 versions |
Times converted from UTC in the StepSecurity timeline. AEST is UTC+10 and Queensland does not observe daylight saving, so these are Brisbane local.
An hour from publication to detection. Four hours to containment. The whole thing started and finished between dinner and bedtime on a weeknight.
The
axios compromise on the 31st of March ran a similar course. Two malicious versions published, pulled within about three hours.
Worms move fast because they have to. They are racing detection, and the registry's immune response is measured in hours rather than weeks. Which means the entire attack window is something you can simply decline to be inside. If your build refuses to install any package version published in the last seven days, you were never a candidate for either of those incidents. You did not need to be paying attention. You did not need to read the advisory. You were asleep in a different timezone and it did not matter.
npm — .npmrc in your project root# days. Requires npm CLI 11.10.0 or newer.
min-release-age=7
Check your version before you trust that line, because on anything older it is an unrecognised key that npm will ignore without complaint. Ours was on 11.6.2 when we went looking, which meant the setting would have done precisely nothing:
$ npm --version
11.6.2
$ npm install -g npm@latest
pnpm — pnpm-workspace.yaml# minutes. 10080 = 7 days. pnpm 10.16+
# On by default at 1440 (24h) since pnpm 11.0.
minimumReleaseAge: 10080
Yarn Berry 4.10+ — .yarnrc.yml# minutes, as a plain number. Duration strings
# like '7d' are silently ignored.
npmMinimalAgeGate: 10080
# exempt your own scoped packages
npmPreapprovedPackages:
- "@your-scope/*"
Bun — bunfig.toml[install]
minimumReleaseAge = 10080
Two things people get wrong. Commit the config file to the repository rather than setting it globally on your laptop, otherwise your machine is protected and your build server, which is the one holding the deploy credentials, is not. And run npm ci in CI rather than npm install, so the lockfile is authoritative and a cooldown-blocked version cannot quietly resolve to something else.
While you are in there, add --ignore-scripts to your CI install. ChainDrop, Shai-Hulud and most of what came before all needed a lifecycle hook to fire. Some packages genuinely require build scripts and you will have to allow those specifically, which is a mild nuisance and a very good trade.
48 hours or 7 days?Forty-eight hours would have covered every npm worm to date, and it is an easier sell to a team that wants patches promptly. Seven days is what we would set for a client project, because the cost of lagging a week behind on a minor version is close to zero and the cost of being in the blast radius is not. Pick whichever you will actually leave switched on.
Be clear-eyed about what this does not do. A cooldown is worthless against a compromise that sits undiscovered for a month, and it does nothing about the poisoned versions already pinned in your lockfile. It also puts you behind on genuine security patches, so when a real CVE lands you will need to override it deliberately for that one package. That override should be a conscious decision someone makes, which is rather the point.
The bare minimum before you take real data
Everything above is about code you did not write. This part is about the code you did, or asked for, and it is the shorter and more uncomfortable list.
None of it is advanced. It is the set of things that need to be true before an application stops being a demo and starts holding somebody else's information, and the reason it is worth writing down is that an agent will not raise any of it unprompted.
- No secrets in the repository, and rotate anything that was ever in there. Git history counts. A key that was committed in March and removed in April is still a key an attacker can read, and ChainDrop was specifically hunting for exactly this.
- Every query parameterised, without exception. An 82% pass rate sounds respectable until you count the queries in a real application. At a hundred queries that is eighteen chances.
- Output encoding on anything a user can influence. This is the 15% number, and it is where we would start looking. If you have dangerouslySetInnerHTML anywhere near user input, that is the first thing to open. This site uses it, for our own copy, which we write and control — that distinction is doing all the work.
- Authorisation checked on the server, every time. Agents are good at hiding a button and calling it access control. Something enforced only in the browser is not enforced. Every endpoint needs to answer whether this particular user may do this particular thing to this particular record, and it needs to answer it server-side.
- Row-level security switched on if you are on Postgres or Supabase. The default posture of an anon key is more generous than most people expect, and it is very easy to ship a working app where the client can read the whole table. Test it as an unauthenticated user before you believe otherwise.
- Rate limits on anything that costs money or sends mail. Contact forms, password resets, anything hitting a paid API. Without one, your form is somebody's spam relay and your API bill is somebody's afternoon.
- Errors that say nothing useful. Stack traces, SQL fragments and file paths belong in your logs. Sentry or something like it should be catching them server-side; the browser gets a sentence and an ID.
- A dependency cooldown, a committed lockfile, and scripts off in CI. As above. Five minutes.
- Someone watching production. Not a dashboard nobody opens. Alerts that reach a human, tested at least once by deliberately breaking something.
If an agent wrote most of the application, treat that list as a review pass rather than a checklist to tick. Read the auth code yourself. Read the queries. Read whatever touches user-supplied strings on their way to a page. The rest can usually be taken on trust, at least until it cannot.
The cost that turns up later
There is a version of this argument that lands as an objection to using AI to write code, and that is not the argument.
The generation step got cheap. Everything downstream did not, and security review is the part of downstream that fails quietly. A missing rate limit does not break the build. Nobody files a ticket about output encoding. It goes to production, works perfectly for eleven months, and then does not.
So the honest position is that AI-assisted development needs more review discipline than the old way, not less, and the time it saves should be partly spent on the thing it made riskier. Set the cooldown this afternoon. It is one line and it would have covered every npm worm so far. Then go and read your own auth code, slowly, as though somebody else wrote it.
Because somebody else did.
Sources and further reading
Everything above is linked where the claim is made. Collected here for anyone who wants to read the primary material end to end.
- Spring 2026 GenAI Code Security Update — Veracode, 24 March 2026. 150+ models, pass rates by language and CWE.
- Vibe Coding's Security Debt: The AI-Generated CVE Surge — Cloud Security Alliance, 4 April 2026. Georgia Tech CVE tracking, Apiiro enterprise figures, slopsquatting.
- ChainDrop supply chain compromise: anatomy of a self-propagating worm — Microsoft Threat Intelligence, 4 August 2026.
- ChainDrop npm Worm: Bun-loaded CI/CD credential harvester — StepSecurity, August 2026. Minute-by-minute timeline, package counts, harvesting scope.
- Inside the keyv npm supply chain compromise — Snyk. How valid provenance ended up on a malicious release.
- Shai-Hulud worm compromises npm ecosystem — Unit 42, Palo Alto Networks.
- The Shai-Hulud 2.0 npm worm — Datadog Security Labs, November 2025.
- Widespread supply chain compromise impacting the npm ecosystem — CISA alert, 23 September 2025.
- The npm threat landscape: attack surface and mitigations — Unit 42. Includes the March 2026 axios compromise.
- npm supply chain attacks: 1.2M malicious packages — Sonatype figures for 2025 and 2026.
- Configuring minimum release age across npm, pnpm and yarn — Matteo Collina. Exact keys, units and minimum versions.
- npm introduces minimumReleaseAge — Socket, on the 11.10.0 release.
- AI coding agent productivity debates: the 2026 paradox — collects the GitLab and LinearB figures.
- AI hasn't shifted the bottleneck from coding to code review — The New Stack, arguing the other side.