What happened
On March 31, 2026, two malicious versions of axios — the most-downloaded HTTP client
on npm at over 70 million weekly downloads — were published to the public registry from a
compromised maintainer account. The versions were axios@1.14.1 on the modern release
line and axios@0.30.4 on the legacy 0.x line. Both pulled in a freshly staged
dependency, plain-crypto-js, that existed solely to deliver a postinstall payload.
StepSecurity flagged the malicious tarballs the same day. By the time the npm registry pulled the packages, the bad versions had already been resolved into countless lockfiles and CI caches. Microsoft published its analysis on April 1, 2026 attributing the operation to a North Korea-nexus cluster it tracks as Sapphire Sleet. Google's Threat Intelligence Group tracks the same activity as UNC1069 and links the stage-2 implant to WAVESHAPER.V2, a direct evolution of the WAVESHAPER backdoor previously attributed to UNC1069.
If you have ever wondered why we keep harping on lockfiles, pinned dependencies, and --ignore-scripts in CI, this is the incident the slide deck was written for.
1. The timeline
The interesting part is the staging. The attacker did not just compromise axios — they pre-built the dependency they were going to inject, weeks in advance.
- Stage 0 — pre-staging. A clean, harmless
plain-crypto-js@4.2.0is published to npm to establish publish history and reduce scrutiny on the package name. There is nothing malicious in this version. It exists purely to make a follow-up release look like a normal patch bump rather than a brand-new package. - March 31, 2026. A maintainer account with publish rights to
axiosis compromised. The attacker publishesplain-crypto-js@4.2.1— the same package, now with a maliciouspostinstallhook — followed minutes later byaxios@1.14.1andaxios@0.30.4. The two axios releases are surgical manifest-only changes: a single new entry addingplain-crypto-js@^4.2.1todependencies, with no source changes anywhere else in the package. - March 31, 2026. StepSecurity flags the malicious tarballs. The npm registry pulls all three packages; the maintainer's session is invalidated.
- April 1, 2026. Microsoft Security Blog attributes the operation to Sapphire
Sleet. Google GTIG attributes the same activity to UNC1069 and links the stage-2 implant to
WAVESHAPER.V2. Advisories land in GitHub Security Advisories, the Snyk database, and the
axios/axios#10636upstream post-mortem.
The window between the first malicious publish and registry takedown was a matter of hours — but
by then every CI runner that ran a fresh npm install against an unpinned dependency had
already resolved the poisoned tree.
2. The attack chain
The malicious code was not in axios itself. The two compromised axios versions are byte-for-byte
identical to 1.14.0 and 0.30.3 apart from a single dependencies entry in package.json:
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0",
"plain-crypto-js": "^4.2.1"
} When npm resolves that range it lands on plain-crypto-js@4.2.1 — the malicious
successor to the harmless 4.2.0 stub the attacker had pre-staged. That package's package.json declares a postinstall hook:
"scripts": {
"postinstall": "node ./scripts/setup.js"
} The script runs automatically on npm install, with no user interaction. It reads process.platform, fetches a platform-specific stage-2 binary from a Sapphire
Sleet-controlled C2 at sfrclak[.]com, writes it to a persistent location on disk, and
executes it. The stage-2 payload is a cross-platform Remote Access Trojan with native builds for
macOS, Linux, and Windows. The C2 IP 142.11.206.73 is hosted on Hostwinds, a VPS provider
Sapphire Sleet has reused across multiple operations, and the second-stage fetch happens over plain
HTTP on port 8000.
3. What the RAT actually does
The post-infection behavior is consistent across platforms. On first run the implant collects host
telemetry — hostname, username, OS version, list of running processes, the presence of common
developer tools (git, aws, gcloud, kubectl, docker) — and ships it back to the C2.
Then it walks well-known credential locations and exfiltrates anything it finds:
~/.aws/credentialsand~/.aws/config~/.config/gcloud/credentials.dband any application default credentials~/.kube/config~/.npmrc(for npm publish tokens — note the recursive opportunity)~/.docker/config.json~/.ssh/id_*private keys- GitHub CLI hosts file at
~/.config/gh/hosts.yml - Browser cookie stores for major browsers (Chromium-derived and Firefox), targeted at GitHub, GitLab, npm, PyPI, Docker Hub, and major cloud consoles
- Cryptocurrency wallets — MetaMask extension state, Keychain entries with wallet keywords, and standard CLI wallet locations
After the initial sweep the implant settles into a long-poll C2 loop and accepts commands for arbitrary shell execution, file upload, and additional payload staging. The persistence mechanisms are typical for the family — a launchd plist on macOS, a systemd user unit on Linux, a scheduled task plus a Run-key entry on Windows.
4. Indicators of compromise
The publicly published indicators are short, specific, and easy to hunt for. None of them are subtle.
- Network. Any DNS resolution or outbound connection to
sfrclak[.]com, or any traffic to the IP142.11.206.73. Stage-2 fetches happen over plain HTTP on port 8000 against that host. - HTTP user-agent. The implant ships with a hardcoded IE8 / Windows XP user-agent string reused identically across the macOS, Linux, and Windows variants. Per Elastic Security Labs, this is the single most reliable network-side detection signature in the toolkit — trivially detectable on any modern egress stream and effectively impossible for a developer machine to produce legitimately in 2026.
- npm tree.
npm ls plain-crypto-jsreturning anything anywhere in the resolution graph. - Lockfile. Any
package-lock.json,pnpm-lock.yaml, oryarn.lockthat resolvesaxiosto1.14.1or0.30.4, or that containsplain-crypto-jsat any version.
One nasty detection wrinkle: a forensic snapshot of node_modules taken after the install can mislead you, because the resolved tree no longer matches what was actually
installed. The lockfile is the source of truth for "did we ever pull this," not the resolved tree on
disk. For platform-side IOCs (file paths, persistence artifacts, WAVESHAPER.V2 hashes), pull the current
Microsoft, GTIG, and Elastic Security Labs advisories directly — they are the authoritative IOC sources
for this incident and they are being updated as the investigation progresses.
5. Attribution
Microsoft attributes the operation to Sapphire Sleet, the cluster Google's GTIG tracks as UNC1069 — a financially motivated North Korea-nexus actor active since 2018, with a long history of targeting cryptocurrency engineers, exchanges, and the open-source packages those engineers depend on. The same activity overlaps with what other vendors track as STARDUST CHOLLIMA, BlueNoroff, Alluring Pisces, CageyChameleon, and CryptoCore.
Google GTIG and Mandiant link the stage-2 implant to WAVESHAPER.V2, a direct
evolution of the WAVESHAPER macOS / Linux backdoor previously attributed to UNC1069. The
combination of UNC1069 tradecraft, WAVESHAPER.V2 lineage, and the reuse of Hostwinds VPS
infrastructure (142.11.206.73) gives this attribution unusually high confidence for
an npm supply-chain incident, where toolkits are often shared between clusters.
6. If you pulled a bad version, do this
The order matters — work top to bottom.
- Find the blast radius. Grep every lockfile in every repo for
axios/-/axios-1.14.1.tgz,axios/-/axios-0.30.4.tgz, andplain-crypto-js. Don't trust resolvednode_modules— see the detection wrinkle above. Also check every developer machine, not just CI. - Treat affected hosts as compromised. Any machine that successfully ran
npm installagainst a poisoned tree must be considered to have run the RAT. Pull the host off the network, image it for forensics, and rebuild from a known-good baseline. - Rotate every credential the host could see. AWS access keys, GCP service account keys, Azure client secrets, kube credentials, GitHub PATs and OAuth tokens, npm tokens (this matters — a stolen npm token is how the next package gets backdoored), Docker registry credentials, SSH keys, and any browser-stored session cookies for SaaS consoles. Use the cloud audit logs to look for use from unfamiliar IPs in the affected window.
- Downgrade and pin. Lock
axiosback to1.14.0or0.30.3inpackage.jsonandpackage-lock.json. Regenerate the lockfile from clean caches. Add a CI guard that fails the build if either bad version reappears. - Hunt for persistence and lateral movement. Walk the IOC list above on every reachable host. In CI, check for any new GitHub Actions workflows, modified build scripts, or recently pushed branches that touch credentials. If your CI uses a shared runner, assume the runner host is compromised, not just the build that pulled the package.
- Notify customers if you ship software. If you produced any artifacts during the bad window, your customers need to know. Sign your statement, attach a fresh SBOM, and give them the IOCs they need to hunt their own environments.
7. The DevSecOps lessons
Every npm supply-chain incident produces the same takeaways and the same teams ignore them. This one is no different. The controls that would have blunted the blast radius are mundane and well-known.
- Pin transitive dependencies, not just direct ones. A direct pin on
axios@1.14.0would not have helped a project that rannpm updateor that used a caret range. A committed lockfile and a CI step that runsnpm ciinstead ofnpm installwould have. Make surenpm ciis what your CI actually runs — many pipelines still callnpm installby habit. - Disable lifecycle scripts by default. The exact one-liners that would have
neutralized this incident:
npm ci --ignore-scripts,pnpm install --ignore-scripts,yarn install --mode=skip-build. Better still, set it as the default for the whole environment:npm config set ignore-scripts trueand the equivalent for pnpm and Yarn. The cost is that a small number of legitimate packages depend on postinstall hooks (esbuild,sharp,node-sass) — manage those with explicit allowlists or dedicated build steps that run outside the main install. The benefit is that a hijacked dependency cannot run code at install time, which is by far the most common npm attack vector. - Understand why SCA and reachability tools do not save you here. This is the part most teams miss. Reachability-based SCA (Snyk Code, Socket, Endor Labs, Semgrep Supply Chain) reasons about whether vulnerable code is reached at application runtime. EPSS scores rank vulnerabilities by exploit-in-the-wild likelihood. Both assume the malicious code lives inside the imported library and runs when your app calls it. In this incident the malicious code ran at install time, before your application existed as a process — and the axios library itself was byte-identical to the clean version. Reachability says "not reachable." EPSS says "no exploit." Both are right and both are useless. The control that defeats install-time attacks is install-time isolation, not runtime analysis.
- Use a private registry mirror with a quarantine window. Tools like Artifactory, Nexus, or Sonatype Repository Firewall let you ingest from npm with a delay (commonly 24–72 hours) so newly published versions cannot be pulled until they have aged. Almost every npm compromise of the last three years has been caught and removed inside a 24-hour window. A quarantine policy turns this incident into a non-event.
- Generate SBOMs and diff them. A new top-level dependency landing in your tree should be a CI failure that requires human approval, not something that silently appears in the next build artifact. If you are already producing SBOMs (and you should be — see our SBOM practical guide), wire a diff check that fails on unexpected new packages.
- Maintainer 2FA is necessary, not sufficient. The compromised account had 2FA.
Phishing kits that proxy TOTP codes are everywhere. Hardware-backed keys (WebAuthn / FIDO2) on
the npm account, plus npm's
--require-2fa-for-publishsetting, raise the bar substantially. Encourage maintainers of packages you depend on to enable both. For your own internal packages, require both. - Run developer machines like production. The credentials on a developer laptop
unlock more of your environment than the credentials on most production hosts. Treat them
accordingly — full-disk encryption, EDR, enforced patching, and a credential store that does not
hand out plaintext on first read. The implant in this incident harvested everything in
~/.aws/credentialsin a single syscall. There is no reason that file should be plaintext on disk in 2026.
8. The meta lesson
The interesting thing about this incident is not the malware — it is mediocre and we have all seen
ten variants. The interesting thing is how unremarkable the attack vector has become. A maintainer
of the most-downloaded HTTP client on npm gets phished, the attacker publishes a patch-level bump
with one extra line in dependencies, and within hours every CI runner that runs npm install is one postinstall away from a remote shell. There is no novel exploitation, no zero-day,
no creative chaining. The ecosystem is structured to make this exact attack cheap and scalable.
The good news is that the controls that defeat it are also cheap. Lockfiles, npm ci, --ignore-scripts, a quarantining proxy, and a habit of treating new transitive
dependencies as a security event. Most teams we audit have one or two of these in place. Almost
none have all five. The teams that did all five woke up on April 1 with nothing to do.
The short version
On March 31, 2026, axios@1.14.1 and axios@0.30.4 shipped from a hijacked
maintainer account with one extra dependency, plain-crypto-js@^4.2.1 — a malicious successor to a clean stub the attacker had
pre-staged as 4.2.0. The package's postinstall script delivered a
cross-platform Remote Access Trojan calling back to sfrclak[.]com (142.11.206.73, Hostwinds VPS, stage-2 over plain HTTP on port 8000). Microsoft
attributes the operation to Sapphire Sleet; Google GTIG tracks it as UNC1069 and links the stage-2
implant to WAVESHAPER.V2. If you pulled a bad version, grep your lockfiles, treat affected hosts
as compromised, rotate every credential they could see, and downgrade to 1.14.0 or 0.30.3. If you did not, this is the moment to wire npm ci --ignore-scripts, lockfile diffs, and a quarantining registry proxy into your
build pipeline. They are the cheapest insurance in DevSecOps and they would have made this
incident a non-event.
Want a pipeline this attack would have bounced off?
We harden CI/CD against supply-chain attacks — lifecycle-script isolation, lockfile diff gates, quarantining registry proxies, SBOM and signing. Senior DevSecOps engineers, no SDRs.