The Payload Is in the Post-Install Script and the SVG, Not the Manifest
Two live campaigns exploit the same gap: your controls check who published a package, while the hostile code runs from a lifecycle hook that inherits every credential on the runner.
Two execution surfaces need two different controls
chalk and debug belong in a different bucket from axios, because they execute in a different place. The first pair is reached almost entirely at install and build time, which in practice means a lifecycle script running inside a CI container. The compromised axios release, at roughly 100 million weekly pulls, added a plain-crypto-js dependency. Its post-install script contacted a hardcoded command-and-control endpoint and staged a cross-platform remote-access trojan for Windows, macOS and Linux. axios is a different shape of problem: a runtime dependency sitting in the production request path.
Here is what a post-install script actually inherits on a build runner: NPM_TOKEN, GITHUB_TOKEN, a cloud OIDC session, artifact-signing keys, and network reach into the internal registry. That is not a tainted artifact. It is arbitrary code execution inside your most credentialed compute. Any runner that resolved an affected version needs full secret rotation. A rebuild does nothing.
Where the reporting disagrees, and who is right
Cyberpresso leads its remediation with pinning: enforce npm ci, commit lockfiles for services and container builds, disable dependency-bot auto-merge. CyberScoop's analysis of the same campaign pushes back on that emphasis, and the pushback holds up. A lockfile pins the malicious version too. Reproducibility is not intent verification. The two controls with the best cost-to-coverage ratio are the ones that change what executes and when:
| Control | Stops install-time payload | Stops runtime payload | Where it breaks |
|---|---|---|---|
Lockfiles plus npm ci | No | No | Pins the bad version deterministically |
ignore-scripts plus allowlist | Yes | No | Native builds: node-gyp, sharp, prisma, esbuild |
| 7-14 day version cooldown at the proxy | Mostly | Mostly | Adds patch latency; needs an audited bypass lane |
| Provenance and sigstore attestation | No | No | Attacker held real publish rights, so attestation is valid |
| Short-lived OIDC credentials, no ambient secrets | Impact only | Impact only | Requires real pipeline rework |
The cooldown is the underrated one. The community currently catches malicious publishes in hours to days, so a 7-14 day minimum release age enforced at the registry proxy turns a zero-hour compromise into a non-event. The price is patch latency, and that needs an override path for a known-exploited fix.
The variant that never touches package.json at all
Elastic Security Labs documented a parallel DPRK campaign that defeats the manifest-centric controls outright. Developers receive fully functional coding-challenge repositories; one was seeded through Elastic's own community Slack #jobs channel. The payload sits as Base64 chunks inside HTML comments within SVG flag images, reassembles, and executes on the first npm run dev or npm start. Nothing malicious appears in package.json. Software-composition analysis, lockfile pinning and registry allowlists all report green.
What lands is a four-module OTTERCOOKIE stack. Module 2 is the one that decides severity: a recursive sweep for .env files, SSH configs, AWS configs and shell history. A laptop holding a static AWS key in a .env is a straight line from a take-home assignment to the production account. Module 3 is a Socket.IO remote-access trojan, so the exfiltration channel looks like a normal real-time web app.
Static scanning inspects manifests and registry artifacts. Both of these campaigns execute from somewhere else, so the only controls that hold are isolation and short-lived credentials.
The detection that is cheap to build is behavioral, not signature-based: alert when a node or npm child process opens an outbound WebSocket, when a recursive glob touches ~/.aws or ~/.ssh, or when any process outside sanctioned CLI paths reads ~/.aws/credentials.
What to do
Set ignore-scripts=true in every CI .npmrc, with a named allowlist for the handful of packages needing native builds (node-gyp, sharp, prisma, esbuild class).
Query build logs and image manifests for axios, debug, chalk, typo-crypto and transitive plain-crypto-js in the compromise window by end of week, then rotate registry tokens, cloud credentials and signing keys for every runner that resolved them.
Enforce a 7-14 day minimum release age for third-party dependency updates at the registry proxy this sprint, with a documented emergency bypass for known-exploited fixes.