Security

The Axios Supply Chain Attack — What Happened and How to Prevent It

100 million weekly downloads compromised overnight. The .npmrc config, lockfile discipline, and update strategy that would have protected you.

Nicolas
NicolasFounder & CEO
March 31, 202612 min read

What You Need to Know

March 31, 2026

  • 1

    The attack: An attacker hijacked the npm account of axios's lead maintainer and published two backdoored versions (1.14.1 and 0.30.4) that install a cross-platform remote access trojan via a hidden dependency.

  • 2

    The impact: Axios is downloaded ~100 million times per week. Both the latest and legacy tags were poisoned, meaning any npm install axios pulled the malware.

  • 3

    The fix: Downgrade to axios@1.14.0 or 0.30.3, delete node_modules/plain-crypto-js, rotate every secret on affected machines, and harden your .npmrc.

What Happened

On March 30, 2026, an attacker compromised the npm account of jasonsaayman — the lead maintainer of the axios project — and changed the registered email to an attacker-controlled Proton Mail address. Within hours, they published two backdoored versions that injected a phantom dependency called plain-crypto-js@4.2.1. StepSecurity's research team was first to identify and report the compromise.

This dependency is never imported anywhere in the axios source code. Its sole purpose is to execute a postinstall script that acts as a cross-platform RAT dropper. The payload targets macOS, Windows, and Linux with separate binaries — each pre-built and staged 18 hours before the attack went live. Socket's deep-dive analysis found additional packages distributing the same malware through vendored dependencies.

The dropper uses two layers of obfuscation — reversed Base64 encoding with padding character substitution and XOR cipher with the key OrDeR_7077. After execution, the malware deletes itself and replaces its own package.json with a clean version to evade forensic detection. Every trace was designed to self-destruct. Snyk has a detailed technical breakdown of the full payload chain.

Attack Timeline

Mar 30, 05:57 UTC

Clean plain-crypto-js@4.2.0 published to npm (staging)

Mar 30, 23:59 UTC

Malicious plain-crypto-js@4.2.1 published with RAT payload

Mar 31, 00:21 UTC

axios@1.14.1 published with plain-crypto-js injected (tagged latest)

Mar 31, 01:00 UTC

axios@0.30.4 published (tagged legacy) — both branches poisoned

Mar 31, ~03:00 UTC

npm removes malicious versions after community reports

Why It's Worse Than You Think

Most teams I talk to had the same initial reaction: “We don't use axios.” And they're wrong. Axios is a transitive dependency for thousands of packages. As The Hacker News reported, this is “potentially one of the largest in npm history” by blast radius. Your project might not list it in package.json, but there's a good chance something in your node_modules depends on it.

Run npm ls axios in any non-trivial Node.js project. You'll likely see it appear multiple times through packages you never associated with HTTP clients.

Popular packages that depend on axios

@slack/web-api

Slack's official Node.js SDK — 6.7M weekly downloads

@nestjs/axios

NestJS HTTP module used by millions of backend services — 4.1M weekly downloads

twilio

Twilio Node.js SDK uses axios for all HTTP requests — 3.8M weekly downloads

@sendgrid/client

SendGrid's email API client ships axios as a direct dependency — 3.7M weekly downloads

contentful

Contentful CMS delivery SDK depends on axios — 1.1M weekly downloads

plaid

Plaid banking API client bundles axios — 550K weekly downloads

The real lesson

Any package with "axios": "^1.x" in its own package.json could have resolved to 1.14.1 during a fresh install. The caret range is a trust contract — and the npm registry broke that trust for 2-3 hours.

The .npmrc Config That Would Have Saved You

Three lines in a .npmrc file at the root of your project. That's the difference between “compromised” and “not my problem.” Commit this file to your repo so every developer and CI runner inherits the same policy.

# .npmrc — supply chain hardening
# Requires npm v11.10.0+ (shipped February 2026)
# Don't install packages published less than 7 days ago.
# Value is in days. This is the single most effective line here.
# The axios attack was caught within hours — this buys you a week.
# See: github.com/npm/cli/pull/8965
min-release-age=7
# Block postinstall scripts by default.
# The axios RAT executed via a postinstall script in plain-crypto-js.
# When a legit package needs scripts, allowlist it explicitly.
ignore-scripts=true
# Pin exact versions. No more ^1.13.6 resolving to 1.14.1.
# Every version bump becomes a deliberate, reviewable decision.
save-exact=true

The min-release-age=7 setting is the single most impactful line. Introduced in npm v11.10.0 (February 2026), it tells npm to refuse any package version published less than n days ago. The value is in days — 7 means one week. The axios attack was detected and pulled within 2-3 hours. A 7-day delay means you never would have installed it. You can read the original implementation PR for technical details. Note that there's a known bug with tilde (~) version ranges — another reason to use save-exact=true.

If you also use Dependabot for version updates, you can align its cooldown settings with your min-release-age so both your local installs and your automated PRs respect the same waiting period. pnpm and Yarn have had similar features since late 2025 — Andrew Nesbitt wrote a great overview of the ecosystem-wide shift toward release-age gating.

Disabling scripts blocks the primary malware execution vector. The axios RAT ran entirely through a postinstall script in plain-crypto-js. With ignore-scripts=true, that script never executes. Some legitimate packages need postinstall scripts (sharp, esbuild, prisma) — allowlist those explicitly:

# For packages that legitimately need postinstall scripts,
# allowlist them in .npmrc:
# (npm v10.9+ supports per-package script allow)
script-allow[]=sharp
script-allow[]=esbuild
script-allow[]=@prisma/client

And save-exact=true means npm install axios writes "axios": "1.14.0" instead of "axios": "^1.14.0". No caret, no surprise resolutions. Every version bump becomes a pull request you actually review.

Stop Deleting Your Lockfile

I need to say this clearly because I've watched it happen too many times: your package-lock.json is not a build artifact. It's a security contract. The pnpm team calls it “part of your supply chain contract” — and they're right. It pins every resolved version — direct and transitive — to exact hashes. When your lockfile says axios@1.13.6, that's exactly what gets installed. No surprises. No “helpful” upgrades.

Here's the scenario that keeps me up at night: a developer hits a merge conflict in package-lock.json. It's 4,000 lines of JSON diff. They don't want to deal with it. So they delete the lockfile, run npm install, get a fresh one, push, and move on. Problem solved, right?

Wrong. That fresh npm install resolves every ^ range to the latest matching version. If you did that during the 2-3 hour window when axios@1.14.1 was tagged latest, congratulations — you just installed a RAT on your machine and committed its hash into your repo for every other developer and CI runner to pick up.

The right way to resolve lockfile conflicts

  1. 1. Accept either side: git checkout --theirs package-lock.json
  2. 2. Regenerate cleanly: npm install (this updates the lockfile to match your package.json)
  3. 3. Stage and commit. The lockfile now reflects your merged package.json with verified resolutions.

Use npm ci everywhere that isn't a developer adding a package

Your CI/CD pipeline, your Docker builds, your staging deployments — all of these should use npm ci, not npm install. The difference matters:

# In your CI pipeline (GitHub Actions, etc.)
# ALWAYS use npm ci, NEVER npm install
- name: Install dependencies
run: npm ci
# npm ci:
# ✅ Fails if package-lock.json is missing
# ✅ Fails if lockfile doesn't match package.json
# ✅ Never modifies the lockfile
# ✅ Deletes node_modules and installs from scratch
#
# npm install:
# ❌ Creates/updates the lockfile
# ❌ May resolve to newer (compromised) versions
# ❌ Silently succeeds even with lockfile drift

Protect the lockfile with a pre-commit hook

If you want to make lockfile deletion a hard error instead of relying on code review to catch it:

# .husky/pre-commit — block lockfile deletion
#!/bin/sh
# Check if package-lock.json is being deleted
if git diff --cached --diff-filter=D --name-only | grep -q "package-lock.json"; then
echo "❌ ERROR: package-lock.json is being deleted."
echo " Lockfiles are a security contract. Resolve merge conflicts instead."
echo " Run: git checkout --theirs package-lock.json && npm install"
exit 1
fi

The Art of Not Updating

There's an anxiety in our industry around staying current. Dependabot opens a PR, you merge it without reading the diff, and you feel responsible. “Latest” is not always “safest.” Sometimes the most secure thing you can do is not update.

The axios attack exploited exactly this instinct. The malicious version was tagged latest. Anyone who blindly ran npm update or had Dependabot set to auto-merge got burned. The developers who were “behind” on their axios version? They were fine.

A sane update strategy

1

Schedule updates, don't react to them

Pick a cadence — biweekly, monthly, quarterly. Update dependencies in a dedicated branch with a dedicated review. No drive-by version bumps in feature branches.

2

Never auto-merge Dependabot or Renovate

These tools are scanners, not approvers. Let them open PRs. Review those PRs like any other code change. Read the changelog. Check the diff. Ask: “Do I actually need this update?”

3

Let the community be your canary

The min-release-age=7 setting enforces this automatically. But even without it, waiting a few days after a major version bump gives security researchers, automated scanners, and the community time to flag problems. The axios attack was caught in hours. A 7-day buffer is more than enough.

4

Separate security patches from feature updates

Security patches get fast-tracked. Feature updates wait for the scheduled window. Not every version bump is equal — treat them differently.

The counterintuitive truth

The teams that got hit hardest by this attack weren't the ones running old versions. They were the ones who prided themselves on always being on latest. Being a week behind the bleeding edge isn't technical debt — it's a security policy.

Indicators of Compromise & Remediation

If you ran npm install on any project between approximately 00:21 and 03:00 UTC on March 31, 2026, check for these indicators. Full IOC lists are available from Semgrep and SOCRadar.

IndicatorValue
Malicious versionsaxios@1.14.1, axios@0.30.4
Phantom dependencyplain-crypto-js@4.2.1
C2 serversfrclak.com:8000 (path /6202033)
Attacker emailifstap@proton.me
User-Agentmozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
macOS payload/Library/Caches/com.apple.act.mond
Windows payload%PROGRAMDATA%\wt.exe (copied PowerShell)
Linux payload/tmp/ld.py

Immediate steps if affected

1

Downgrade to axios@1.14.0 or axios@0.30.3

2

Delete node_modules/plain-crypto-js/ from every project

3

Reinstall with npm install --ignore-scripts

4

Rotate every credential on affected systems: npm tokens, cloud keys (AWS, GCP, Azure), SSH keys, CI/CD secrets, database passwords, API keys

5

Check for the platform-specific payload files listed in the IOC table above and scan with your endpoint security tool

Frequently Asked Questions

Related Resources

Build Secure Software Faster

TurboDocx helps teams automate document generation, e-signatures, and workflows with enterprise-grade security built in.

Nicolas
NicolasFounder & CEO