If you ran npm install anywhere between midnight and 3am UTC on March 31, 2026, and your project uses axios, there's a real chance you pulled a remote access trojan onto your machine without doing anything wrong. No sketchy link clicked. No suspicious email opened. Just a routine package install.
I’m writing this on April 1st, and I want to be clear — this is not a joke. The axios npm package, one of the most used JavaScript libraries in the world, was compromised overnight. The lead maintainer’s account was hijacked. Two backdoored versions were published. Any developer or CI/CD system that ran a fresh install during that window got malware. Automatically. Silently.
So let me walk you through what actually happened, how the attack technically worked, and what you need to do right now if you’re not sure whether you’re affected. I’ll try to keep this readable for everyone — not just security researchers.

What Is axios and Why Does It Matter Here
If you’ve done any JavaScript development in the last several years, you’ve used axios. It’s the standard library for making HTTP requests from JavaScript — calling an API, fetching data from a backend, posting JSON to a server. Works in the browser and in Node.js. React projects use it. Express backends use it. It gets pulled in as a transitive dependency by so many other packages that plenty of developers don’t even realize it’s in their project.
At the time of the attack, axios had roughly 100 million weekly downloads on npm. That number is worth sitting with for a second. Most npm packages get a few thousand downloads a week. Popular ones get a few million. Axios is operating at a different scale entirely. That scale is exactly what made it a target.
And that’s also what makes this incident so hard to quantify. Even a 2–3 hour exposure window at 100 million downloads a week is a massive number of potential installs. We’ll come back to how many were actually confirmed affected.
What Exactly Happened — The Timeline
This attack was planned. Not thrown together overnight — well, sort of, but what I mean is the preparation started days before the actual payload dropped.
On March 30, 2026 at 05:57 UTC, someone registered a new npm package called plain-crypto-js under a ProtonMail address (nrwise@proton.me) and published version 4.2.0. This version was completely harmless. No malicious code. No suspicious scripts. Just a clean, boring package sitting on the registry, building a small publishing history.
Then at 23:59 UTC on March 30, the same account pushed version 4.2.1 of plain-crypto-js. This one had the payload.
At 00:21 UTC on March 31, two malicious axios versions went out: axios@1.14.1 tagged as latest, followed by axios@0.30.4 tagged as legacy at 01:00 UTC. Both published from the account of jasonsaayman — the lead maintainer of axios — but with an email changed to a ProtonMail address. The legitimate releases always came from jasonsaayman@gmail.com. The malicious ones came from ifstap@proton.me.
Elastic Security Labs filed a security advisory on GitHub at 01:50 UTC, and npm pulled the packages around 03:15 UTC. So the malicious packages were live for roughly three hours.
Three hours is enough. Way more than enough.
The Clever Part — How the Attack Was Structured
The attacker did not touch axios’s actual source code. That’s the first thing to understand. If they had modified the axios library itself, it would have been far more visible — code reviews, diffs, checksums. Instead they did something smarter.
They added one new dependency to axios’s package.json: plain-crypto-js@^4.2.1. That's it. One line. And that dependency did all the work.
Here’s why this matters for anyone who isn’t deeply in the npm weeds: when you install a package, npm also installs all its dependencies. And when a dependency has a postinstall script, npm runs it automatically. No confirmation. No prompt. It just runs. The plain-crypto-js@4.2.1 package had this in its configuration:
"scripts": {
"postinstall": "node setup.js"
}So the moment you ran npm install axios and got version 1.14.1, npm also fetched plain-crypto-js, and that package's setup.js file executed on your machine. You didn't see it. You didn't approve it. It was just part of the install process.
The clean decoy version published 18 hours earlier — plain-crypto-js@4.2.0 — gave the malicious package a brief, innocent-looking history on the registry. Makes it less likely to trip simple heuristics that flag brand-new packages.
The pre-staging, the separate dependency, the clean decoy — this was not opportunistic. StepSecurity researchers said it themselves: “Three separate payloads were pre-built for three operating systems. Both release branches were hit within 39 minutes. Every trace was designed to self-destruct.”
Inside the Dropper — Two Layers of Obfuscation
The setup.js file is what security researchers call a "dropper" — its job is to figure out what environment it's running in and fetch the actual malware.
It wasn’t written in plaintext. The attacker used two layers of encoding to hide what it was doing from static analysis tools that scan packages before they’re published. Layer one: string reversal combined with Base64 encoding. Layer two: an XOR cipher using the key OrDeR_7077. All the important stuff — domain names, shell commands, file paths — was stored in an encoded array and decoded at runtime.
Snyk’s security team broke this down on March 31. Honestly, the obfuscation itself is not that hard to reverse once you know what to look for. But the point isn’t to be unbreakable — it’s to get past the automated scanners that check new npm packages. And it worked long enough.
After decoding its string table, the dropper checks which operating system it’s running on and contacts the command and control server at sfrclak[.]com:8000. The outbound request is disguised to look like it's going to packages.npm.org — the real npm registry domain. So in your network logs, at first glance, the traffic looks like a normal package install. That's a deliberate choice and a good one from the attacker's perspective.
Then it downloads the actual RAT. And this is where I’d say the attack becomes genuinely unusual.
The RAT — One Malware, Three Languages
Most supply chain attacks that get this far deliver a pretty simple payload. A coin miner. A basic info-stealer. A reverse shell. What this attacker deployed was more thought-through than that.
They wrote three separate implementations of the same remote access trojan: PowerShell for Windows, C++ for macOS, Python for Linux. Same C2 protocol across all three. Same command set. Same beacon timing. Same spoofed user-agent in the HTTP requests back to the server. Elastic Security Labs, who published their full technical analysis on April 1, called it “one RAT to rule them all” — a single malware spec, implemented three times in three platform-native languages.
On macOS, it drops into /Library/Caches/com.apple.act.mond — disguised as an Apple system cache daemon. On Windows, it uses a renamed PowerShell binary called wt.exe inside %PROGRAMDATA%, which at first glance looks like Windows Terminal. On Linux, it's a Python script at /tmp/ld.py, which at least on Linux doesn't need much of a disguise because /tmp is full of random stuff anyway.
Once running, it immediately starts reconnaissance: enumerating your user directories, filesystem roots, running processes — and sends all of that back to the attacker’s server.
But here’s the part that bothers me more than the RAT itself.
The Cleanup — How the Attack Erases Itself
After setup.js runs, it deletes itself using fs.unlink(__filename). Then it takes the package.json from plain-crypto-js — the file that contained the malicious postinstall hook — and replaces it with a clean version that has no scripts entry at all.
So after the attack runs, if you go look at node_modules/plain-crypto-js/package.json, you see nothing wrong. The file looks like an ordinary package with no lifecycle scripts. The setup.js dropper is gone. Without looking at your lockfile or your npm install logs, there's almost no way to tell from the node_modules folder alone that anything happened.
I spent a while trying to understand exactly how the forensic chain works here and, well, it’s uncomfortable. The primary evidence that survives is in your package-lock.json or yarn.lock — lockfiles record exact resolved versions, so plain-crypto-js@4.2.1 would show up there. And your npm installation logs would have the timestamp. But if you're on a CI system that doesn't retain logs and doesn't commit lockfiles — which I know is more common than it should be — you might genuinely not know.
Huntress’s security operations center, who were already investigating this at 10 PM on March 30 (US time), found the malicious plain-crypto-js dependency nested inside a WordPress scripts package: @wordpress/scripts/node_modules/plain-crypto-js. Not because someone directly installed axios. Because axios is a transitive dependency of another package which is a dependency of the WordPress scripts. That's how deep this goes in the dependency tree.
How Many Machines Were Actually Hit
Huntress confirmed 135 endpoints in their own partner network contacting sfrclak[.]com during the exposure window. Across all operating systems — Windows, macOS, Linux.
One hundred and thirty-five is just what one security vendor saw in their customer base. The actual number across all developers and companies who ran npm install between midnight and 3am UTC is unknown. Given that 100 million downloads happen every week and CI pipelines run around the clock, my guess is the real number is significantly higher. But the malware cleans up after itself, so plenty of affected machines will never be identified.
Sophos’s Counter Threat Unit detected the first activity in their telemetry at 00:45 UTC on March 31 — about 24 minutes after the malicious axios@1.14.1 was published. Widespread impact by 01:00 UTC. That's how fast it spread.
And no, this isn’t just about individual developers running installs on their laptops. CI/CD systems run 24/7. If your build pipeline depends on axios and it ran a fresh install that night, you need to check what happened.
What You Need to Do Right Now
If you’re reading this and you’re not sure whether you’re affected, start here.
Check your lockfiles first. Open your package-lock.json or yarn.lock and search for any of these: axios@1.14.1, axios@0.30.4, or plain-crypto-js@4.2.1. If any of those strings appear and you installed packages during the exposure window, treat that machine as fully compromised.
Check your CI pipeline logs. Look for any job that ran npm install or npm ci between approximately 00:15 UTC and 03:30 UTC on March 31, 2026. If you find one, check what version of axios got resolved.
Block the C2 domain. Add sfrclak[.]com to your firewall blocklist immediately if you haven't already. Doesn't matter whether you were affected — just block it.
If you were affected: do not just uninstall axios and run npm audit. The malware ran during installation. The RAT was already dropped to your filesystem before you removed the package. You need to re-image the machine or restore from a clean backup taken before March 30. Then rotate everything that could have been accessible during install: npm access tokens, AWS keys, SSH private keys, anything in .env files, cloud credentials for GCP and Azure, any CI/CD secrets stored in environment variables.
This is the step most developers will skip because it feels excessive. It’s not excessive. The RAT immediately started doing reconnaissance — reading your filesystem, your running processes. If you had API keys or tokens sitting in environment files, you should assume they were read and sent to the attacker’s server.
Safe versions to use: axios@1.14.0 is the last legitimate 1.x release, published on March 27, 2026 with full SLSA provenance. For the older API, axios@0.30.3 published on February 18, 2026 is the last clean 0.30.x release.
A Security Detail That Should Have Caught This Faster
There’s something here that I think deserves more attention than it’s getting in the coverage I’ve seen.
Legitimate axios releases are published through GitHub Actions using OIDC — that’s short for OpenID Connect, which creates a verifiable link between the published npm package and the specific GitHub Actions run that built it. The package gets what’s called an SLSA provenance attestation. You can look at an npm package and verify: yes, this was built by a specific automated workflow, from a specific commit, at a specific time.
The malicious versions had none of this. They were published directly from the CLI, with no provenance, from a changed email address. Security teams who specifically check for missing provenance on new versions of major packages caught this quickly. Most teams don’t check this. Most developers don’t even know this feature exists.
So there’s a gap. npm has the tooling for verifiable supply chain provenance and most of the ecosystem doesn’t use it or enforce it. That gap is what made this attack possible, or at least delayed detection.
The Bigger Picture — This Is Happening More Often
This is part of a pattern that’s been building for months.
Earlier in March 2026, a group called TeamPCP compromised Trivy, a popular open-source security scanner. From there they got into LiteLLM, a widely-used Python library for working with LLMs. Then Telnyx was hit. Now axios. Whether the same group is behind all of these is not confirmed. As of today, April 1, nobody has publicly attributed the axios attack to a specific actor. The attacker used ProtonMail addresses for everything and the C2 domain hasn’t been linked to any known group.
But even if they’re separate incidents, the pattern is the same: compromise a trusted maintainer account, publish a malicious version, and rely on the ecosystem’s automatic trust of registered packages.
The 2021 ua-parser-js attack worked the same way. So did the event-stream incident from 2018. Every few years, someone does this to a widely-used package, the community reacts, tooling gets a little better, and then it happens again to a different package.
The honest thing to say here is that I don’t think this problem gets solved without real structural changes to how npm handles maintainer account security. Individual maintainers of packages that millions of people depend on shouldn’t have single-account-no-MFA access to publish. The blast radius is too large. And long-lived npm access tokens — which are probably how this attacker got in, though that’s still not confirmed — are a specific issue that npm has been pushing maintainers to migrate away from for years now, with mixed results.
What This Means for How You Manage Dependencies
A few practical things worth thinking about after this, not just for the immediate incident.
Pin your dependencies to exact versions. Using ^1.14.0 in your package.json means you automatically get 1.14.1 on the next install. That caret operator is how most projects work and it's also how this attack spread automatically. Exact version pinning — "axios": "1.14.0" — doesn't prevent this entirely, but it means every version change is a deliberate decision.
Commit your lockfiles to version control. I know this sounds basic but I’ve worked on projects where the package-lock.json was in .gitignore. Don't do that. Your lockfile is the forensic record of exactly what got installed. Without it, you can't audit anything after the fact.
Look at whether your team can switch to pnpm. This is a bit of a side note — actually more than a side note. pnpm, by default, disables lifecycle scripts (postinstall, preinstall, etc.) for all packages except those you explicitly allowlist. This specific attack mechanism — a malicious postinstall hook — would not have executed under pnpm's default configuration. The dropper would have been installed into node_modules but never run. That's a meaningful default.
And look at tools that monitor npm packages for suspicious changes. Socket Security published the first public alarm about this compromise in a tweet that Huntress’s SOC saw at 10:56 PM on March 30, US time. Socket’s monitoring system had already detected that plain-crypto-js was suspicious. Early warning like that matters.
What We Still Don’t Know
As of April 1, 2026, there’s no CVE assigned yet — someone filed a request on GitHub on March 31 and it’s presumably being processed. The axios team hasn’t published a full post-mortem. We don’t know exactly how jasonsaayman's account was compromised: phishing, stolen token, credential stuffing, or something else. The fix for that probably affects what recommendations come out of it.
The attack is also still developing, sort of. Nobody has said the C2 server is down. Nobody has confirmed how many total machines connected to it. The investigation by Elastic, Huntress, Sophos, StepSecurity and others is ongoing. By the time you read this, there may be new findings — possibly a CVE number, possibly an attribution, possibly a full account from the axios maintainers themselves.
What I can say is that if you check your lockfiles today and find nothing, you’re probably fine. If you find those version strings and you haven’t rotated your credentials yet, you need to stop what you’re doing and start that process.
One More Thing Worth Saying
The open source ecosystem runs on trust. Developers trust that packages on npm are what they say they are. CI pipelines run without human review of every install. That trust is not unreasonable — it’s how the whole thing functions at scale. But it does mean that when a trusted account gets taken over, the damage happens before anyone notices.
The tools to reduce this risk exist. Provenance attestations, exact version pinning, lockfile audits, pnpm’s script restrictions, real-time package monitoring. None of them are perfect and none of them are widely used enough.
This incident is going to get attention for a few weeks, there will be calls for better npm security, some tooling will improve — and then the next one will happen to a different package. Unless the structural stuff actually changes: mandatory MFA for maintainers of high-download packages, short-lived tokens by default, provenance required for new major releases. We’ll see.
For now, check your lockfiles. Update to axios@1.14.0. Rotate your tokens. And maybe read up on SLSA provenance so you know what to look for next time.
Checking If You’re Affected — Quick Reference
Search for these exact strings in your package-lock.json or yarn.lock:
axios@1.14.1— malicious, taggedlatestaxios@0.30.4— malicious, taggedlegacyplain-crypto-js@4.2.1— the malicious dependency that delivered the RAT
Check your CI logs for any npm install or npm ci runs between 00:00 and 03:30 UTC on March 31, 2026.
Block sfrclak[.]com at your firewall.
Safe to use: axios@1.14.0 for the 1.x API, axios@0.30.3 for the 0.30.x API.
If you find an affected machine: re-image it, don’t just uninstall the package.