npm's supply chain is broken — the Axios attack explains why

🇧🇷 Ler em Português
Table of Contents

Over eighty million downloads per week. Three hours of exposure. A single npm install was all that stood between your CI runner — with all its deploy credentials — and a remote access trojan.

On March 30, 2026, Axios — arguably the most widely used HTTP library in the JavaScript ecosystem — was compromised. Not through a code vulnerability. Not through a protocol bug. Through an attacker who stole a maintainer’s credentials and published malicious versions directly to npm.

The most disturbing part: the model that enabled this attack isn’t a bug. It’s how npm was designed to work.

1) What happened

The attack followed a surgical execution:

March 30 — The attacker publishes plain-crypto-js@4.2.0 on npm. Clean version. Zero malicious code. The goal: build history on the registry so the package wouldn’t look suspicious.

March 31 (~18h later) — Publishes plain-crypto-js@4.2.1. Now with a malicious postinstall hook and an obfuscated dropper.

March 31 (same day) — Using stolen credentials from Axios’s lead maintainer (jasonsaayman), changes the account email to ifstap@proton.me and publishes two versions:

  • axios@1.14.1
  • axios@0.30.4

Both add plain-crypto-js@4.2.1 as a direct dependency in package.json. The dependency is never imported in code. It’s a phantom dependency — it exists solely so npm would execute its install script.

~3 hours later — Scanners (Socket.dev, npm) detect and remove the malicious versions.

Three hours sounds short. It isn’t.

Axios has over 80 million downloads per week. That’s thousands of downloads per minute. In ~3 hours of exposure, we’re talking about potentially tens of thousands of installations — each executing the malware automatically, with zero developer interaction.

On April 1, the Google Threat Intelligence Group (GTIG) publicly attributed the attack to UNC1069, a North Korea-nexus threat actor linked to the Lazarus Group (BlueNoroff). Microsoft confirmed, referring to the same actor as Sapphire Sleet. This wasn’t an opportunistic hacker. It was a nation-state operation with financial motivation — the same kind of group behind billion-dollar cryptocurrency exchange heists.

2) Anatomy of the attack

Phase 1: The decoy package

The attacker published a clean version of plain-crypto-js one day before. Registries and scanners are less aggressive with packages that already have history. A clean version creates the illusion of legitimacy.

Subtle detail: the name plain-crypto-js mimics the popular crypto-js. It’s not exact typosquatting, but if a human sees this dependency in a diff, they probably won’t raise an eyebrow.

Phase 2: The phantom dependency

In the compromised Axios versions, the only change was in package.json:

{
  "dependencies": {
    "plain-crypto-js": "4.2.1"
  }
}

No import. No require. No reference in source code.

This is a red flag that should be programmatically detectable: a dependency declared in the manifest that no file in the project uses.

Phase 3: The dropper

plain-crypto-js@4.2.1 declared a postinstall hook:

{
  "scripts": {
    "postinstall": "node setup.js"
  }
}

setup.js was double-obfuscated — reversed Base64 + XOR cipher with key OrDeR_7077. When decoded, the script:

  1. Detected the OS (Windows, Linux, macOS)
  2. Connected to the C2: sfrclak[.]com:8000 (IP: 142.11.206.73)
  3. Downloaded a platform-specific payload
  4. Executed the payload
  5. Erased its own evidence — replaced setup.js and package.json with clean stubs

That last step is particularly dangerous. After execution, inspecting node_modules would show nothing abnormal. The infection was invisible post-facto.

Phase 4: The RAT

The final payload was a cross-platform Remote Access Trojan with:

  • Credential, token, and secret exfiltration
  • Persistence (survived reboots)
  • Continuous C2 communication channel
npm install axios@1.14.1
  └─> resolves plain-crypto-js@4.2.1
       └─> postinstall: node setup.js
            └─> detects OS
            └─> GET sfrclak[.]com:8000/payload
            └─> executes RAT
            └─> erases evidence

3) The real target isn’t your machine — it’s your CI

Let’s be direct about the real impact.

On a developer’s machine, this attack steals local credentials. Bad, but contained.

In CI/CD, the damage is on another level.

A typical CI runner has access to:

  • AWS/GCP/Azure credentials via environment variables — the RAT reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY straight from the process
  • Production deploy tokens — including access to Kubernetes clusters, container registries, S3 buckets
  • Application secrets — API keys, OAuth tokens, TLS certificates
  • GitHub/GitLab tokens — which may grant write access to other repositories

An attacker with these secrets can deploy malicious code to production. Exfiltrate customer data. Pivot to other internal systems.

And here’s the worst part: many runners use persistent secrets instead of temporary credentials. A deploy token that never expires and was exfiltrated at 3 AM remains valid weeks later — long after anyone notices the attack.

This isn’t dev credential theft. It’s potential access to a company’s entire production infrastructure.

4) The trust model that enables all of this

The problem isn’t Axios. It’s the model.

Exponential transitive trust

When you add axios to your project, you’re not just trusting Axios:

your-project
  └─ axios (you trust this)
       └─ follow-redirects (do you know this?)
       └─ form-data
            └─ asynckit (and this?)
            └─ combined-stream
                 └─ delayed-stream (and this?)
            └─ mime-types
                 └─ mime-db (and this?)
       └─ proxy-from-env
       └─ plain-crypto-js ← injected by attacker

Each dependency is a link of implicit trust. The attacker didn’t need to compromise Axios — they just added one more link to the chain.

An average JavaScript project has hundreds of transitive dependencies. You trust all of them. You’ve audited none.

Semver ranges: blind auto-update

Most projects use ^ in package.json:

{
  "axios": "^1.14.0"
}

This means: “accept any patch or minor version from 1.14.0 onward.” When the attacker published 1.14.1, any subsequent npm install installed the malicious version with zero code changes in your project. No PR. No diff. No review.

Publishing outside CI

The attacker published directly to npm with a stolen token. This completely bypassed all CI/CD, code review, and security automation in the Axios repository. The code never went through a pull request.

npm doesn’t require that a package was built in a verifiable pipeline. Anyone with a valid token publishes whatever they want.

Postinstall hooks: automatic execution of arbitrary code

npm runs install scripts automatically. It’s a legitimate feature — packages with native binaries need to compile. But it’s also the primary attack vector in supply chain attacks.

Think about it: you run npm install and npm executes arbitrary code from hundreds of packages on your system. With your user’s permissions. With access to all your environment variables.

I think npm should disable scripts by default. The operational cost of explicitly enabling scripts for the few packages that actually need them is negligible compared to the risk of executing arbitrary code from hundreds of dependencies on every install. Bun already does this. Deno never allowed it. npm is behind.

Why hasn’t any of this been fixed?

This trust model has had known problems for nearly a decade. So why does it still work this way?

Backwards compatibility. Disabling postinstall scripts by default would break thousands of legitimate packages. npm can’t do this without a massive migration plan — and the organization has never prioritized one. Every breaking change in npm affects millions of projects. The result is inertia.

Decentralized governance. npm is maintained by GitHub (Microsoft), but the ecosystem is decentralized by design. There’s no central authority that can impose stricter publishing rules without community pushback. Proposals like “mandatory provenance” run into maintainers who publish from their personal machines — and who don’t want (or can’t) set up CI pipelines.

Economic incentives. npm Inc. (now GitHub) profits from adoption. Any friction in the publishing flow — mandatory 2FA, mandatory provenance, package cooldowns — reduces adoption at the margin. The economic incentive is to make publishing as easy as possible. Security is cost, not revenue.

The average developer doesn’t think about this. Most engineers treat npm install as an inert operation — like downloading a file. Risk perception is low because attacks are invisible when they work. It only makes the news when someone catches it. How many smaller attacks went undetected?

The result is an ecosystem where everyone knows the model is fragile, but no one has sufficient incentive to break it and rebuild. Until the cost of inaction exceeds the cost of action. Axios may not be that inflection point. But each incident gets closer.

5) This isn’t new — and it’s accelerating

IncidentYearVectorNovel techniqueImpact
event-stream2018Transferred control to malicious contributorLong-term social engineeringBitcoin wallet theft
ua-parser-js2021Stolen npm credentialsDirect account takeoverCryptominer + password stealer
colors.js / faker.js2022Sabotage by maintainer (protestware)Insider threat (no external breach)Mass build breakage
Shai-Hulud (chalk, debug)2025Phishing + self-propagating wormAutonomous propagation via CI/CD500+ packages, 2B downloads/week
Axios2026Stolen classic token + phantom dependencyAnti-forensics + nation-state actorCross-platform RAT, 80M+ downloads/week

The pattern is clear. Every year: bigger packages, more sophisticated techniques, shorter windows.

The evolution is telling: attacks that once relied on social engineering (convincing a maintainer to transfer control) now exploit automation, compromised credentials, and even autonomous propagation via CI/CD. Shai-Hulud in 2025 was a worm. Axios used anti-forensics to erase traces. The next iteration will be worse.

The current model of “publish fast, scan later” isn’t sustainable. npm needs to evolve toward a model where verifiable provenance is a requirement, not optional.

6) How to protect yourself

There’s no silver bullet. The defense is defense in depth — layers that, combined, significantly reduce risk.

6.1) Versioning strategy: lockfile + automation

Pinning all dependencies to exact versions sounds safe, but in practice creates another problem: you stop receiving legitimate security patches. Frozen dependencies age fast.

The approach that works in production is strict lockfile + controlled automated updates:

# CI/CD: always install from lockfile, never resolve new versions
npm ci

In package.json, use semver ranges normally — but never run npm install in CI. The lockfile is your anchor.

For updates, use Renovate or Dependabot to open automatic PRs when dependencies have new versions. This gives you:

  • Visibility: each update becomes a PR with diff, changelog, and security scan
  • Control: you review before merging — no silent upgrades
  • Speed: legitimate security patches arrive quickly

The rule: the lockfile is truth; updates are explicit and auditable.

6.2) Disable lifecycle scripts

Configure the project’s .npmrc:

# .npmrc
ignore-scripts=true

For the few packages that need scripts (e.g., esbuild, sharp, bcrypt), enable them explicitly:

npm rebuild esbuild  # runs scripts only for this package

This blocks the primary supply chain attack vector. The cost is minimal — the vast majority of packages don’t need scripts.

6.3) Behavioral auditing (not just CVEs)

# npm audit checks known CVEs — necessary but insufficient
npm audit

# Socket.dev analyzes behavior: scripts, network, obfuscation
npx socket scan

The distinction is critical. npm audit only finds vulnerabilities already catalogued. The Axios attack was detected by Socket.dev before it had a CVE — because the scanner identified anomalous behavior: new package with postinstall, obfuscated code, network access during installation.

If you rely only on npm audit, you’re protected only against past attacks.

Red flags for manual or automated review:

  • Declared dependency but never imported — phantom dependency, exactly like this attack
  • Postinstall script in a pure JavaScript package — packages without native binaries rarely need scripts
  • Obfuscated code — Base64, eval, atob, XOR in library code is a red flag
  • Network access during installation — fetch/http calls in install scripts
  • Recently created package as dependency of a popular package — the combo of “new + used by big package” is suspicious
  • Recent email/ownership change on maintainer — sign of account takeover

None of these signals is proof of attack in isolation. Combined, they’re near-certain.

6.4) Verifiable provenance (SLSA and Sigstore)

I need to explain this better, because it’s the most important long-term defense.

Provenance answers a simple question: who built this package, from which code, in what environment?

Without provenance, when you install axios@1.14.1, there’s no way to know if it was built by the official repository’s CI or if someone ran npm publish from a compromised laptop. The two are indistinguishable.

SLSA (Supply chain Levels for Software Artifacts) is a framework that defines assurance levels. At the most useful level for npm (SLSA Build L3), the package includes a signed attestation proving:

  • It was built on a specific CI runner (e.g., GitHub Actions)
  • From a specific commit in a specific repository
  • Without human intervention in the build process

Sigstore is the signing infrastructure that makes this possible. It uses ephemeral certificates tied to OIDC identities — no need to manage GPG keys.

In practice, this lets you verify that a package was generated by a trusted automated pipeline — not by someone running npm publish from a laptop. In the Axios case, the malicious versions were published manually, with no valid provenance. A provenance check would have been a strong indicator of compromise.

Here’s how it works:

# Check if your project's packages have valid provenance
npm audit signatures

If the Axios attacker had published manually, as they did, the malicious version would have no provenance statement. An automated check in CI would catch it:

# In CI pipeline
npm audit signatures || exit 1

Today, few packages publish with provenance. But this is changing — and it should be an npm requirement, not optional. A registry that accepts anonymous publishing without build attestation is a registry that invites supply chain attacks.

6.5) Package cooldown

Don’t automatically install releases less than 72 hours old. The Axios attack lasted ~3 hours. A 24-hour cooldown would have been enough to avoid exposure.

In practice, there are concrete ways to implement this:

With a private registry (Artifactory, Verdaccio, Nexus): configure a quarantine policy that only mirrors packages from the public npm registry after N hours. The package exists upstream, but only becomes available to your devs after the cooldown period. This is the most robust solution for companies.

With Renovate: configure minimumReleaseAge in renovate.json to delay update PRs:

{
  "packageRules": [
    {
      "matchUpdateTypes": ["patch", "minor"],
      "minimumReleaseAge": "3 days"
    }
  ]
}

This doesn’t prevent a dev from installing manually, but ensures automated updates only happen after the cooldown.

With Dependabot: there’s no native cooldown support, but you can combine it with a GitHub Action that checks the package’s publish date and blocks the merge if it’s too recent.

The principle: time is the best scanner. Most supply chain attacks are detected within hours or days. An intentional delay transforms you from victim to observer.

6.6) Granular tokens and CI-only publishing

The Axios maintainer had 2FA/MFA enabled. It didn’t help. The actual vector was a classic long-lived token (NPM_TOKEN) configured as an environment variable in CI. Classic npm tokens don’t enforce MFA for publishing — once stolen, the attacker publishes freely.

Worse: Axios had already migrated to OIDC Trusted Publishing via GitHub Actions. But when both a classic token and OIDC credentials are present, the npm CLI prioritizes the token. This rendered OIDC ineffective — the attacker published manually with the stolen token, completely bypassing pipeline protections.

The lesson: eliminate classic long-lived tokens. Use OIDC Trusted Publishing as the sole publishing mechanism. Revoke all classic tokens. Configure granular tokens with limited scope (specific package, IP range, short expiration) only when OIDC isn’t possible. Regularly verify that legacy tokens aren’t forgotten in CI environment variables.

7) What I’d do as Tech Lead

If I were responsible for supply chain security on a team, I’d implement this tomorrow:

1. Lockfile as contract. npm ci in CI, no exceptions. PRs that change package-lock.json get an automatic label and mandatory review.

2. Scripts disabled by default. .npmrc with ignore-scripts=true committed to the repo. Explicit allowlist for packages that need rebuild.

3. Behavioral scan on PR. Socket.dev or equivalent running on every PR that touches dependencies. Not a substitute for npm audit — a complement.

4. Provenance as gate. npm audit signatures in the pipeline. Package without verifiable provenance in a critical dependency? Fail the build.

5. Ephemeral secrets in CI. No persistent AWS_SECRET_ACCESS_KEY in runner environment variables. OIDC federation with AWS/GCP/Azure — credentials that expire in minutes, not months.

6. Internal proxy/registry. Larger companies: consider a private npm registry (Artifactory, Verdaccio) that enforces policies before packages reach devs. Cooldown, mandatory scan, allowlist.

The total cost? A sprint or two of setup. The cost of not doing it? Ask anyone who’s had production credentials exfiltrated by an npm install.

8) For security teams and platform engineering

If you’re on a security or platform team, the recommendations above are necessary but not sufficient. The question for you isn’t “how does my project protect itself” — it’s “how do I ensure no project in the organization is exposed”.

Internal registry with policies. An npm proxy (Artifactory, Nexus, Verdaccio) that sits between developers and the public npm registry. The proxy enforces: mandatory cooldown, behavioral scan before mirroring, allowlist of approved packages for critical dependencies. No new package enters the company’s environment without passing through this gate.

Selective mirroring. Instead of mirroring all of npm, maintain a curated list of approved packages. Packages outside the list require explicit approval. This is more restrictive, but for regulated organizations (fintech, healthcare, critical infrastructure) it may be the only acceptable model.

Dependency observability. Dashboards that show: which packages were updated in the last 7 days across each project? Which have verifiable provenance? Which use postinstall scripts? This transforms supply chain security from reactive (“was someone compromised?”) to proactive (“where are we exposed?”).

Secrets policy as code. CI secrets should not be manually configured by devs. Use OIDC federation (AWS, GCP, Azure all support it natively) so runners receive ephemeral credentials tied to the specific workflow. If a runner is compromised, a token that expires in 15 minutes limits the blast radius drastically.

Incident response plan for supply chain. The question isn’t “if” — it’s “when”. Have a runbook ready: how to detect that a malicious package entered the environment, how to identify which projects and environments were affected, how to rotate secrets at scale, and how to communicate internally. Axios gave ~3 hours of window. Without automation, that’s not enough time to respond manually.

9) If you were affected — checklist

If your project ran npm install between March 31, 2026 and the removal of the malicious versions:

Check exposure

# Search lockfile
grep -E "axios@(1\.14\.1|0\.30\.4)|plain-crypto-js" package-lock.json

# Check installed version
npm ls axios
npm ls plain-crypto-js

Remediate

# Downgrade to safe version
npm install axios@1.14.0

# Clean and reinstall
rm -rf node_modules package-lock.json
npm install

Rotate EVERYTHING

Every credential, token, API key, and secret accessible on the affected machine or environment. No exceptions:

  • AWS/GCP/Azure credentials
  • GitHub/GitLab tokens
  • Database credentials
  • SSH keys
  • Deploy tokens
  • Secrets in environment variables

Assume total compromise. It’s cheaper to rotate credentials that weren’t exfiltrated than to discover months later that they were.

Audit network

# IOCs:
# Domain: sfrclak[.]com
# IP: 142.11.206.73
# Port: 8000

grep -E "sfrclak|142\.11\.206\.73" /var/log/syslog

Also check firewall, proxy, and DNS logs for outbound connections to the C2.

10) Conclusion

The npm ecosystem was built for speed and convenience. Installing is trivial. Updating is automatic. Publishing is fast. This drives productivity. And it’s exactly what attackers exploit.

But here’s the question this incident should force the community to answer: whose responsibility is it?

The individual developer who ran npm install without auditing 800 transitive dependencies? The maintainer who, even with 2FA enabled, had a classic long-lived token exposed in CI? npm, which prioritizes legacy tokens over OIDC, executes arbitrary scripts by default, and accepts publishing without provenance? The funding model that expects volunteers to maintain critical infrastructure with enterprise-grade security?

The comfortable answer is “everyone’s”. The honest answer is that the model places most of the burden on those with the least power to act — the end developer — while those who control the registry and set the defaults could solve most of the problem with configuration changes.

The defenses exist. Lockfiles, provenance, behavioral scanning, disabled scripts, ephemeral secrets. None is perfect alone. Combined, they turn a three-hour attack into a non-threat.

But it shouldn’t be necessary to build an individual fortress to compensate for the structural weaknesses of a platform.

npm could, today, deprecate classic long-lived tokens and require OIDC or granular tokens with expiration. It could disable postinstall scripts by default and require explicit opt-in. It could make verifiable provenance a requirement for packages above N downloads. None of these changes is technically impossible — Bun and Deno have already implemented variants. What’s missing is political will and the pressure from incidents like this to force the change.

A nation-state group compromised one of the most widely used libraries in the world. The three-hour window was short by luck, not by design. The next one may not be.

In the meantime, every npm install remains an act of trust. And trust, in security, is verified — not assumed.


References:

Comments