Skip to content
T threat.cafe

· Tobias Lindqvist-Rao (editorial persona) · Incident response · on threat.cafe

The axios compromise from an agent operator's seat: which controls would have fired, and when

Replayed against the public timeline of the 31 March 2026 axios compromise, a release-age delay of even one day and install-script blocking would each have prevented the malicious code from running for the entire exposure window. A lockfile with npm ci protected existing projects but not an agent adding axios fresh. A provenance-regression check would have flagged [email protected] but not [email protected]. Advisory-based known-bad lookups arrived at 03:15 UTC, as npm pulled the versions, which makes them a response tool for this incident rather than a preventive one.

The symptom

StepSecurity reported that its Harden-Runner tool flagged anomalous outbound connections in CI runs across several open-source projects, which led to the discovery that two new axios releases pulled in a dependency the library never imports (StepSecurity). StepSecurity puts axios above 100 million weekly downloads. The malicious releases changed no library code. They added plain-crypto-js@^4.2.1 as a dependency, whose postinstall hook ran a dropper that fetched a platform-specific second stage for macOS, Windows or Linux.

The timeline, from primary records

Times are UTC. Publish times come from the npm registry's own time records, retrieved on 15 October 2026 (axios, plain-crypto-js). Removal times come from StepSecurity, which infers the axios unpublish from the registry's modified field, and the advisory time from OSV's record.

2026-03-27 19:01:40  [email protected] published via GitHub Actions trusted publisher, SLSA provenance
2026-03-30 05:57:32  [email protected] published (StepSecurity: clean decoy, no postinstall)
2026-03-30 23:59:12  [email protected] published (malicious postinstall)
2026-03-31 00:21:58  [email protected] published from compromised maintainer account   <- window opens (1.x)
2026-03-31 01:00:57  [email protected] published from same account                     <- window opens (0.x)
2026-03-31 ~03:15:30 npm unpublishes both axios versions; latest returns to 1.14.0 <- window closes
2026-03-31 03:15:49  GHSA-fw8c-xr5c-95f9 "Malware in axios" published
2026-03-31 03:25:11  npm security hold on plain-crypto-js
2026-03-31 04:26:33  [email protected] stub published

Exposure: about 2 h 53 min for 1.14.1, about 2 h 15 min for 0.30.4, and about 3 h 26 min for [email protected] before the security hold. Note one inconsistency in the finder's write-up: StepSecurity dates its own detection to 30 March, while the publish records are 31 March UTC. The likely explanation is a time-zone difference, but the post does not say, so this replay uses registry times only.

Production topology: how an agent meets this

Three installation paths matter, and agents use all three.

  1. Existing project, committed lockfile, npm ci. The lockfile pins [email protected] or earlier. No new resolution happens.
  2. Existing project, npm install or a lockfile refresh. A caret range such as ^1.14.0 resolves to the newest matching version, which during the window was 1.14.1.
  3. New dependency. An agent asked to "add an HTTP client" runs npm install axios, which resolves latest. During the window, latest pointed at 1.14.1.

Path 3 is the agent-specific exposure. A human adds axios to a project occasionally. An agent scaffolding projects can do it many times a day, at any hour, and in auto mode no person reads the command first. The window ran from 00:21 to 03:15 UTC, which is evening in the Americas and morning in Asia and Australia: working hours somewhere.

Control replay

Release-age delay (cooldown): fires for the whole window

StepSecurity lists release-age settings across tools: Dependabot cooldown, npm min-release-age (npm 11.10.0+), pnpm's minimum release age (10.16+), Yarn's npmMinimalAgeGate (4.10+) and Bun's minimumReleaseAge (1.3+). No malicious version in this incident reached four hours of public age. A one-day delay would have resolved ^1.14.0 to 1.14.0, and [email protected] was itself under a day old. A seven-day delay would have resolved to 1.13.6, published 27 February, because 1.14.0 was only three days old.

Cost: urgent security patches wait too. Every tool listed supports exemptions, which become the attack surface to govern.

Install-script blocking: fires for the whole window

The dropper ran from a postinstall hook. StepSecurity recommends npm ci --ignore-scripts in CI and as a standing policy. With scripts disabled, the malicious dependency would have been written to disk without executing, and axios never imports it.

Cost: packages with native builds or legitimate setup scripts break and need an explicit allowlist. And the control is specific to this delivery method; malware that runs on import would not be stopped by it.

Lockfile plus npm ci: fires for path 1 only

Projects on path 1 were never exposed. Paths 2 and 3 resolve fresh versions by design. A lockfile is a reproducibility tool that happens to give protection, not a policy an agent cannot route around.

Provenance-regression check: fires for 1.x, silent for 0.x

Registry data shows 1.14.0 published by a trusted-publisher binding with an SLSA provenance attestation, while StepSecurity found that 1.14.1 had no trusted-publisher binding and no gitHead. A check that compares each release's publishing path with the previous one would have flagged 1.14.1 from 00:21. But the previous 0.x release, 0.30.3, carries no provenance attestation in the registry, so the same check sees no change for 0.30.4. A blanket "require provenance" rule would have caught both, and would also block most of npm, since Aikido counts only 11,001 of the top 51,370 packages publishing through trusted publishing (Aikido, 24 August 2026).

Known-bad lookup against advisories: fires at 03:15

The GitHub malware advisory went live at 03:15:49, within a minute of the unpublish. A hook or scanner that checks GHSA or OSV would have flagged nothing during the window and everything after it. That is still valuable. After 03:15, the same lookup run against lockfiles and CI logs identifies every machine that installed the bad versions, which is the first scoping step of the response. A curated list that ingests finder reports can, in principle, move earlier than the advisory, but only as early as someone reports it.

Egress allowlist: fires at the second-stage fetch

The dropper needed the network to fetch its payload. StepSecurity states that Harden-Runner's egress allowlisting would have blocked the callback and payload fetch; that is the vendor's claim about its own product. The general mechanism holds for any CI runner or sandboxed agent session limited to known registries. Developer laptops rarely run with an egress allowlist, which is why the agent's sandbox setting matters more than the laptop's firewall.

Endpoint detection: unknown

Public sources do not report which EDR products detected the payloads, or when. This replay makes no claim either way.

Timeline versus controls

UTC          00:00   00:21        01:00          02:00          03:15   04:00
                     |1.14.1 live----------------------------->|
                                  |0.30.4 live---------------->|
Cooldown >=1d        [blocks resolution for the entire window-->]
ignore-scripts       [postinstall never runs for the entire window]
Lockfile + npm ci    [protects path 1 only; agents adding axios fresh are exposed]
Provenance check     [flags 1.14.1 ----------------------------->]  (silent for 0.30.4)
Egress allowlist     [blocks second-stage fetch where enforced -->]
GHSA/OSV lookup                                                 [fires 03:15:49 ->]

Root cause at the publishing layer

The attacker published with a credential that still worked outside the pipeline. StepSecurity reports that the maintainer account's email had been changed to an attacker-controlled address and infers a long-lived classic npm token. Trusted publishing was in use for 1.x; token publishing evidently was still allowed. npm documents the fix for maintainers: after enabling trusted publishers, require two-factor authentication and disallow tokens (npm Docs).

Residual risk and limits

This is one incident. Its controls profile depends on a postinstall delivery, a short window and a hijacked maintainer account. A campaign that runs code on import, stays live for weeks, or ships from a compromised CI pipeline with valid provenance would score differently. Aikido's account of the May 2026 TanStack campaign, which shipped with valid SLSA Build Level 3 provenance, is the counter-example for the provenance row.

Score your own stack with the same seven rows, then check which of them applies to an agent running npm install on a new project, which is the path this incident punished most.

For the 03:15-and-after row, the threat.cafe board records incidents like this one with their sources, so a lockfile sweep has something to check against.

Sources

#axios #npm #postmortem #supply-chain #cooldown

All posts