The Publish Token Is the Transport, Not the Package Version
Triage that starts from a list of bad versions bounds yesterday; only credential rotation and short-lived publishing identities bound tomorrow — and two registries have changed their contracts under you.
Self-propagation is the property that reorders triage. The mechanism matters: the payload harvests credentials, and the stolen npm publish tokens become the vector for the next wave. That is how two packages became hundreds inside a day. A blocklist of known-bad versions describes packages that were already republished. It says nothing about which tokens are currently in an attacker's hands.
"We don't depend on those" is the wrong first answer as well. keyv and cacheable sit in caching and HTTP-client territory, underneath got and cacheable-request, so they resolve transitively into trees nobody would describe as cache-dependent. Verify with npm why keyv and npm why cacheable across every repo and every base container image, then cross-reference lockfile resolution timestamps against CI install logs. The exposure question is not whether the package appears in package.json. It is whether any CI job resolved dependencies during the window, and what was sitting in that job's environment.
Publish no longer means installable
GitHub has started scanning every npm package for malware at publish time. That is genuinely good engineering, and it silently voids an assumption baked into thousands of release scripts: that npm publish returning 200 means the artifact is installable. It now means the artifact is queued for an asynchronous scan of unspecified duration.
| Release step | Old assumption | New reality | Fix |
|---|---|---|---|
| Publish, then install to verify | Immediately available | Available after scan completes | Poll registry metadata with exponential backoff and a hard timeout |
| Sequential monorepo publishes with interdependencies | Each publish unblocks the next | Later packages fail resolution | Publish all, then gate on availability of the full set |
| Emergency hotfix release | Minutes | Minutes plus scan latency | Document the new floor in the incident runbook |
Alert on the timeout instead of failing silently. Otherwise the latency gets discovered during a 3am hotfix.
NuGet removes the option to keep doing this
The .NET side of the same problem now has a date. Per Devshot's reporting, new NuGet.org API keys cap at 30 days from August 17, 2026, and every key created before that hard-expires on November 1, 2026. Microsoft points at Trusted Publishing over OpenID Connect, citing the NX/NPM breach where stolen credentials produced 6,000 malicious activations in 36 minutes.
The coverage gap is being reported as a migration. Trusted Publishing supports GitHub Actions and GitLab only. If the publish step runs on Azure DevOps, Jenkins, TeamCity, or Buildkite, no migration exists. What exists is a rotation treadmill to build: a scheduled rotation job, a secret store that supports programmatic update, and an alert when a key ages past about 21 days.
A worm that steals publish tokens and republishes with them makes version triage the smaller job. The rotation should already be finished.
The structural fix is the same on both registries, and it is a workflow-identity change rather than an architecture change: OIDC short-lived credentials plus staged publishing, and delete the long-lived NPM_TOKEN and NuGet key secrets from CI entirely. A worm that steals a credential valid for minutes, scoped to one workflow, gets nothing worth having.
What to do
Rotate every credential reachable from any CI job that installed dependencies during the compromise window — npm tokens, cloud access keys, Actions secrets, registry and database credentials — before confirming exposure, starting today.
Inventory every NuGet.org API key across CI secret stores, developer machines, and release tooling by mid-September, classifying each as OIDC-eligible or rotation-required, and dry-run a publish from every pipeline by mid-October.
Replace immediate post-publish installs in release pipelines with registry metadata polling using exponential backoff and a hard timeout that alerts, this sprint.