Anatomy of the Axios npm Supply Chain Attack — 2 Hours and 54 Minutes

·Operation Risk·7 min read

Translated from the original Korean post. 한국어 원문 보기 →

The last weekend of March, and a very tense hour for JS developers

Early on March 31st, a small earthquake hit the JavaScript ecosystem: axios — a package pulling roughly 100 million downloads a week — had been compromised. News like this shows up somewhere every week, honestly. Normally I'd file it under "another one" and move on. This time I stopped scrolling. The mechanics of the attack, plus who was behind it, made clear this wasn't a prank.

Two hours and fifty-four minutes. That's how long the malicious versions sat on npm. Sounds short. Then you think about how many projects on Earth pull axios as a dependency, and how many CI/CD pipelines ran during that window. Every one of them was a potential infection path. Short exposure does not mean small blast radius — not in a supply chain attack.

How it played out

Staging the ground (March 30)

The attackers set things up a day in advance by publishing a package called [email protected]. The name apes crypto-js. The contents were malware.

Planting it early is a classic detection-dodge. If a brand-new package suddenly shows up in axios's dependency list, someone's going to look twice. But a package that's already been on the registry since yesterday? On a timeline it reads as pre-existing. They spent more effort making the break-in look unremarkable than on the break-in itself.

The actual attack (March 31, 00:21 UTC)

The whole thing hinged on hijacking the jasonsaayman account — one of axios's core maintainers. The attackers got hold of that account's long-lived npm access token and switched the account email to a Proton Mail address they controlled. Changing the email isn't opportunistic access; it's an attempt to take ownership outright.

Here's the part I keep coming back to. That account had OIDC Trusted Publishing enabled, and the attack still worked. OIDC is supposed to mean publishing happens on a trust relationship, no token required. So how did it fall over?

The workflow also carried an NPM_TOKEN environment variable. npm prefers NPM_TOKEN over OIDC. If both are present, the token wins. One stolen token was enough to make the OIDC protection irrelevant.

If you run infrastructure, that's the painful bit. Turning a security feature on and configuring it so it actually has teeth are two separate jobs. Adopting OIDC produces a nice feeling of "we're covered," and the legacy token sitting next to it quietly undoes all of it. New deadbolt on the door, old key still under the mat.

How they shipped the bad versions

They published axios 1.14.1 and axios 0.30.4. Not through GitHub Actions — straight from the npm CLI. Which means the GitHub repo showed nothing. Anyone auditing by reading source saw a perfectly normal day.

The tag handling was slicker still. They pointed both latest and legacy at malicious versions, so npm install axios handed you malware whether you were on the new line or the old one. In package.json, all you'd see is plain-crypto-js quietly added. A routine diff review wasn't going to catch that. Who scrutinizes one extra dependency line every single time?

What the malware did

Execution via the postinstall hook

Installing the package triggered plain-crypto-js's postinstall script. postinstall has been an attack surface in npm forever — merely installing runs arbitrary code. An obfuscated setup.js fingerprinted the OS in the background, then downloaded and ran a platform-appropriate backdoor.

Per-platform infection

OS 동작 방식 저장 경로
Windows PowerShell을 %PROGRAMDATA%\wt.exe로 복사 후 숨김 실행 %TEMP%\6202033.ps1
macOS curl로 Mach-O 바이너리 다운로드 후 백그라운드 실행 /Library/Caches/com.apple.act.mond
Linux Python 백도어 스크립트 다운로드 후 실행 /tmp/ld.py

Covering all three platforms is real work. That's not a one-off stunt — that's "whichever developer we catch, we want them." Every payload phoned home to sfrclak[.]com:8000, collected system information, and could pull down further commands. They weren't just scraping data; they were establishing remote control.

Cleaning up after itself

The anti-forensics is what impressed me, in a grim way. Once the dropper finished, it deleted itself and restored the tampered package.json to its original state. Show up afterward with forensics tooling and the scene looks spotless.

That raises the difficulty of response considerably. If your gut says something's off and you go look, but the room's already been tidied, you can't decide "am I infected?" by checking whether a file exists. For incidents like this, volatile evidence — install-time logs, network traffic — matters far more than after-the-fact file scanning.

Attribution: UNC1069

Google's Threat Intelligence Group attributed the attack to UNC1069, a North Korea-linked threat actor. Their reasoning:

  • The WAVESHAPER.V2 backdoor is an updated build of WAVESHAPER, which UNC1069 has used before
  • The C2 infrastructure links back to their earlier campaigns
  • The group has been financially motivated since 2018, mostly targeting crypto and AI

What bothers me is that a state-linked actor is now aiming squarely at the open source ecosystem. These groups used to go after specific companies or financial networks. Hit a central registry like npm instead, and one package exposes tens of thousands of projects stacked on top of it. Far better return than breaching companies one at a time.

From an attacker's seat, axios is a great target. A package with 100 million weekly downloads is distribution infrastructure in its own right. And the era where defending meant defending your own code is over.

What to do right now

If you installed axios in the early hours of March 31, or your CI/CD ran during that window, work through this list.

1. Check versions and update

npm list axios
npm audit

If axios 1.14.1 or 0.30.4 shows up, swap to a safe version (1.7.8, for example) immediately. Check your lockfile too. Change package.json without regenerating the lockfile and the malicious version comes right back.

2. Rotate every credential

If your environment overlaps that window (March 31, 00:21–03:15 UTC), revoke and reissue every secret and API key. Operate on the assumption the backdoor already grabbed them. "We probably weren't hit" is the most dangerous assumption in operations.

3. Inspect the systems

Look for these files:

  • Windows: %PROGRAMDATA%\wt.exe
  • macOS: /Library/Caches/com.apple.act.mond
  • Linux: /tmp/ld.py

Given the anti-forensics, though, absence proves nothing. Finding a file confirms infection; not finding one confirms only that you didn't find one.

4. Audit transitive dependencies

You may never import axios directly and still have it pulled in by something else. Direct dependencies alone will miss it. Expand the full tree and check.

5. Lock down your npm account

If you maintain packages, revoke long-lived access tokens, and if you're on OIDC, strip NPM_TOKEN out of the workflow entirely. The coexistence of those two is the whole story here. MFA goes without saying.

Wrapping up

The thing I chewed on longest wasn't the tradecraft. It was the shape of the failure: OIDC — a genuinely modern security control — switched on, and one leftover token beside it routed around all of it. Adding a security feature and raising your security posture are not the same sentence, and I've rarely seen a cleaner demonstration.

Two hours and fifty-four minutes. Not a window where the patch supplier failed, but where the weakest link in the chain of trust that publishes packages snapped. Supply chain attacks are a game of finding the weakest link. And that link is usually sitting right next to the thing we told ourselves was handled.

Was this post helpful?

One click helps me write the next one

#axios#npm#supply chain attack#security#UNC1069