• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Back to Blog

Apr 2, 2026 · 13 min read

The axios maintainer had 2FA enabled. North Korea took his npm account anyway.

On March 31, 2026, a North Korean state actor tricked axios's lead maintainer into installing a fake Microsoft Teams update during a staged video call. The maintainer had 2FA enabled. Two hours and 54 minutes later, npm had served a cross-platform RAT to every CI pipeline that rebuilt in the window. Here's exactly how they did it, and what to check right now.

FFlowpatrol Team·Case Study
The axios maintainer had 2FA enabled. North Korea took his npm account anyway.

The click that backdoored a hundred million downloads

He was on a video call with people he believed were real. The CEO of a well-known company was on the other side of the screen — same face, same voice, same posture as the LinkedIn profile he'd been reading for the last week. They'd been talking in Slack for days. The meeting was on his calendar. Everything checked out.

Halfway through the call, a Microsoft Teams prompt told him something was out of date. He clicked the update. Teams kept running. The conversation kept going.

The "update" was a cross-platform remote access trojan called WAVESHAPER.V2. By the time the call ended, the attacker controlled his laptop. By the time he went to bed, two new versions of axios were sitting on npm — 1.14.1 on the modern line and 0.30.4 on the legacy line — and every CI pipeline that rebuilt in the next two hours and fifty-four minutes was pulling a backdoor.

The maintainer had 2FA enabled the entire time. It did not matter. The attacker wasn't logging in from somewhere else — the attacker was logging in from the maintainer's own machine, inside the maintainer's own session, with the maintainer's own auth tokens already loaded.

The attack chain from a fake Slack workspace and staged Teams call to a backdoored axios install in your CI
The attack chain from a fake Slack workspace and staged Teams call to a backdoored axios install in your CI


Who actually got hit

If you're reading this, you almost certainly weren't in the window. The malicious versions were live on npm from 00:21 UTC to 03:15 UTC on March 31, 2026. Most of the world was asleep. Most CI pipelines didn't fire. Most installs landed before or after.

But the reason you weren't hit has nothing to do with your security posture. It has to do with the clock. The attackers picked a quiet Tuesday morning on purpose, but a few hours either way and "you" becomes "everyone."

The people who did get hit fall into a small, predictable shape: builders shipping AI-generated Node backends with ^1.x ranges in package.json, CI pipelines that rebuild on every push and run npm install instead of npm ci, and Vercel/Render-style deployments that rebuild on demand without a committed lockfile. Roughly 100 million weekly downloads of axios on the modern line, and another 83 million weekly downloads on the legacy 0.30.x line. Wiz's cloud telemetry caught indicators of compromise across a meaningful slice of that.

Note

The exact danger window: axios 1.14.1 published at 00:21 UTC on March 31, 2026; npm pulled it at 03:15 UTC. The legacy 0.30.4 followed the same path. If your CI rebuilt in those two hours and fifty-four minutes with a caret range, you should treat the build as compromised until proven otherwise.


The eighteen-hour con

The maintainer was Jason Saayman, axios's lead. In his post-mortem on axios/axios#10636, he described what happened in a single sentence: "Everything was extremely well coordinated, looked legit, and was done in a professional manner." That's the line that should keep you up.

The con didn't start on Slack or in Teams. It started in research. Google Cloud Threat Intelligence and Microsoft attributed the operation to UNC1069, also tracked as Sapphire Sleet — a North Korean cluster with a long history of long-form developer-targeted social engineering. They picked a real, well-known company. They cloned its founder. They cloned the company's branding down to the channel emojis.

Then they built a fake Slack workspace dressed as that company's internal CI org. Channels with realistic names. Posts that linked to real LinkedIn updates from real employees of the impersonated company. Enough texture that opening the workspace for the first time felt like joining a normal engineering org, not walking into a phishing kit.

They scheduled a Microsoft Teams meeting on his calendar. By the time the meeting started, he'd been inside the fake Slack for long enough that the call felt like the obvious next step in an existing conversation. The trust was already built. The Teams update prompt was the only thing the operation actually needed him to click on — and it was the last thing he'd be suspicious of, because everything around it was real-feeling.

That entire choreography is the part of the story that breaks the standard "phishing awareness" mental model. There was no spelling mistake. There was no suspicious link in an email. There was a week of legitimate-feeling collaboration and then one update prompt during a video call.


The click, and why 2FA didn't help

The "Teams update" was WAVESHAPER.V2 — a cross-platform RAT documented by Elastic Security Labs that runs on Windows, macOS, and Linux, talks to its C2 over JSON, and is purpose-built to harvest exactly what a package maintainer's laptop tends to carry: system info, cloud credentials, environment variables, and npm session tokens.

Once it's running on your laptop, the security model collapses in a specific way that's worth being precise about. 2FA protects your password. It does not protect your session. When you log into npm and tick the "remember me" box, npm hands your laptop a token that says "this session is allowed to publish." That token sits on disk. The RAT reads it. The RAT publishes.

From npm's perspective, nothing anomalous happened. A valid session published a new version of a package the session was authorized to publish. The 2FA challenge had already been solved hours earlier by the legitimate human. The attacker never needed to type a password or approve a push notification.

This is the part builders keep getting wrong. Enabling 2FA on your npm account is the floor, not the ceiling. It protects the front door of the website. It does not protect the room your laptop is sitting in.


The phantom dependency

Here's the part of the attack that's almost elegant. The malicious code wasn't in axios itself. If you had read the diff between 1.14.0 and 1.14.1, you would have seen one change: a new entry in package.json.

{
  "dependencies": {
    "follow-redirects": "^1.15.6",
    "form-data": "^4.0.4",
    "proxy-from-env": "^1.1.0",
    "plain-crypto-js": "4.2.1"
  }
}

plain-crypto-js is never imported anywhere in the axios source. Not in lib/, not in dist/, not in tests. It exists in package.json for one reason: when npm install resolves the dependency tree, it pulls plain-crypto-js from the registry, and plain-crypto-js has a postinstall hook.

That hook is what runs WAVESHAPER.V2 on your machine. You don't need to import axios. You don't need to start your server. You don't need to call a single function. The moment npm install finishes, the RAT is already running with your privileges.

The attackers pre-staged the dependency. On March 30 at 05:57 UTC — roughly eighteen hours before the axios push — they published a clean, harmless plain-crypto-js@4.2.0. No payload, no postinstall surprise. The package sat on npm long enough to look like a normal, low-traffic utility. Then at 23:59 UTC on March 30, they pushed 4.2.1 with the malicious postinstall. Twenty-two minutes later, axios 1.14.1 shipped with plain-crypto-js: "4.2.1" as a dependency.

Socket's automated malware detection caught plain-crypto-js@4.2.1 at 00:05:41 UTC — six minutes after publish. Elastic filed a GitHub Security Advisory at 01:50 UTC. npm pulled axios at 03:15 UTC and pulled plain-crypto-js at 03:29 UTC. The detection chain worked. The window was still almost three hours.


The anti-forensics

Once WAVESHAPER.V2 finishes setting up shop on your machine, it does two things that make this attack harder to investigate than a normal supply-chain compromise.

First, the malicious payload self-deletes. The script that ran during postinstall removes itself from disk. If you go looking for the bad code on a compromised machine after the fact, the bad code is gone.

Second, it overwrites its own package.json with a clean version. The plain-crypto-js directory in your node_modules ends up looking like a perfectly normal, postinstall-free utility package. Static scanners that walk node_modules and check for suspicious postinstall hooks find nothing. You have to look at what npm originally fetched — your lockfile, your registry cache — not what's currently sitting in node_modules, to see that anything happened at all.

This is the detail that should change how you think about post-incident triage. "My scanner says the package is clean" is not the same as "this machine wasn't compromised." If your CI rebuilt in the window, the box that ran the build has to be treated as hostile, even if every file on it now looks pristine.


Why your vibe-coded app was specifically in the crosshairs

When an AI tool generates a Node backend for you, it writes a package.json that looks exactly like what it was trained on — the entire public open-source corpus. That corpus uses caret ranges by default, because caret ranges are the npm convention. Your generated file looks like this:

{
  "dependencies": {
    "axios": "^1.14.0",
    "express": "^4.18.2",
    "dotenv": "^16.0.3"
  }
}

That ^ is the trapdoor. It means "install 1.14.0 or any newer compatible version." When your CI runs npm install fresh, npm contacts the registry, asks for the latest matching version, and installs it. On a normal day that's a patch release with bug fixes. On March 31 at 00:30 UTC, that was a backdoor.

A package.json caret range resolving to a poisoned axios version during the attack window
A package.json caret range resolving to a poisoned axios version during the attack window

The fix on paper is trivial. Pin the version. Commit a lockfile. Use npm ci in CI. In practice almost nobody does all three on a vibe-coded project, because the AI tool that generated the project didn't generate a lockfile and the deploy platform that rebuilt it doesn't enforce one.

We keep seeing the same shape on Flowpatrol scans: package.json with caret ranges, no committed package-lock.json or yarn.lock, and a CI step that runs npm install. All three together is the path the axios payload would have walked into your build. Any one of them missing breaks the chain.

If your CI rebuilt between 00:21 UTC and 03:15 UTC on March 31, 2026

Treat that build host as compromised. Rotate every credential it touched — npm tokens, cloud keys, GitHub PATs, anything in environment variables. Don't just bump axios and redeploy. WAVESHAPER.V2 establishes persistent access; "the bad version is gone" is not the same as "the attacker is gone."


What to check right now

Three commands. Two minutes. Run them on every Node project you own.

1. Look for the phantom dependency. This is the single highest-signal check, because nothing legitimate pulls in plain-crypto-js.

npm ls plain-crypto-js

Empty output is good. Anything else — even a transitive reference deep in the tree — means a build on this machine resolved the malicious dependency at some point. Treat the host accordingly.

2. Check your axios version against the known-bad releases. The poisoned versions are 1.14.1 on the modern line and 0.30.4 on the legacy line. Everything else is fine.

npm ls axios

Safe: 1.14.2 or later, anything in the 1.13.x series or earlier on the modern line, anything other than 0.30.4 on the legacy 0.x line. Bad: exactly 1.14.1 or exactly 0.30.4.

3. Search your lockfile for the phantom dep, even if node_modules looks clean. Remember the anti-forensics: the on-disk package rewrites itself. Your package-lock.json is the ground truth for what npm actually fetched.

grep -n "plain-crypto-js" package-lock.json

If grep returns anything at all, the lockfile remembers a fetch you can't see in node_modules anymore. That's the signal.


The real lesson

The temptation after every supply-chain story is to write a five-step hardening checklist and call it a day. The honest version of this one is shorter.

2FA protects the door. It does not protect the room. The maintainer did everything the standard npm hygiene checklist asks for. He had 2FA. He was a long-tenured maintainer of a respected package. He was not a junior contributor with a weak password. None of it stopped a determined nation-state actor who decided his laptop was the cheapest way into a hundred million weekly downloads.

The end-state answer to this is package signing and provenance verification — Sigstore, npm provenance attestation — enforced at install time on every build in the world. That world doesn't exist yet. Adoption is uneven. Most lockfiles don't verify provenance. Most CI runs don't fail closed on missing attestations.

Until that world exists, the only thing standing between your CI and the next three-hour window is a committed lockfile and npm ci. That's a depressingly small lever for the size of the problem. It's also the one you actually control today.


Five things to do before your next deploy

  1. Run the three checks above on every Node project you own. npm ls plain-crypto-js, npm ls axios, grep plain-crypto-js package-lock.json. If any of them return anything, you have an incident, not a maintenance task.

  2. Pin axios to a known-safe version and drop the caret. 1.14.2 or later on the modern line, or anything in the 1.13.x series if you want to stay conservative. On the legacy line, anything other than 0.30.4. Then commit package.json and package-lock.json together in the same change.

  3. Switch every CI step from npm install to npm ci. npm install resolves the dependency graph fresh against the registry. npm ci reads your lockfile exactly. The first one would have shipped you a backdoor on March 31. The second one would not have.


References

  • Google Cloud Threat Intelligence — North Korea-Nexus Threat Actor Compromises Axios NPM Package
  • Microsoft Security Blog — Mitigating the axios npm supply chain compromise
  • Elastic Security Labs — axios: One RAT to Rule Them All
  • Socket — Automated detection of plain-crypto-js@4.2.1
  • StepSecurity — Timeline and IOCs for the axios incident
  • Snyk — axios 1.14.1 / 0.30.4 advisory
  • Wiz — Cloud exposure analysis of the axios compromise
  • Huntress — Supply chain compromise of axios npm package
  • BleepingComputer — Fake Microsoft Teams "update" used to drop RAT on axios maintainer
  • SC Media — Saayman post-mortem on the axios npm compromise
Back to all posts

More in Case Study

One Line of Code Stole Your Emails: The First MCP Supply Chain Attack
Apr 7, 2026

One Line of Code Stole Your Emails: The First MCP Supply Chain Attack

Read more
The Replit Agent Deleted My Database. When I Told It to Stop, It Ignored Me.
Apr 7, 2026

The Replit Agent Deleted My Database. When I Told It to Stop, It Ignored Me.

Read more
Azure Sign-In Log Bypass: Four Bugs That Made Logins Invisible
Apr 6, 2026

Azure Sign-In Log Bypass: Four Bugs That Made Logins Invisible

Read more
  • Commit your lockfile. If you've been told that lockfiles are "noise in PRs," whoever told you that was wrong. The lockfile is the only durable record of what your build actually fetched. Treat it like infrastructure code.

  • Scan what's deployed, not just what's in your repo. A clean package.json doesn't tell you what's running in production right now. Flowpatrol points at a live URL and walks the same chain a real attacker would — exposed endpoints, leaked secrets, and the compounding gaps a single dependency review can't see. A drill finds one bug; a scanner walks the chain.

  • Cybernews — North Korea behind axios npm package compromise
  • axios/axios#10636 — Maintainer post-mortem