Axios npm Supply Chain Attack: Malicious Versions Drop Remote Access Trojan

Axios npm Supply Chain Attack: Malicious Versions Drop Remote Access Trojan

lschvn

Two malicious versions of axios, a library downloaded over 300 million times every week, deeply embedded in the Node.js ecosystem, were published to npm on March 31, 2026, and caught within hours. axios@1.14.1 and axios@0.30.4 shipped with a hidden dependency that did nothing except execute a postinstall dropper. The dropper fetched OS-specific second-stage payloads from a live command-and-control server, then erased itself and replaced its own manifest to hide the evidence.

If you installed either of those versions, assume your system is compromised.

The Attack in Numbers

  • 2 malicious versions published (1.14.1 and 0.30.4)
  • ~3 hours these versions were live on npm before removal
  • ~300M weekly downloads of axios (enormous blast radius)
  • 3 OS targets (macOS, Windows, Linux), all pre-built payloads
  • 18 hours the attacker staged the infrastructure before pulling the trigger

How the Attack Worked

Step 1: Maintainer Account Hijack

The attacker compromised the npm account of jasonsaayman, the lead axios maintainer. They changed the registered email to an attacker-controlled ProtonMail address, then published malicious builds manually via npm CLI, bypassing the project's normal GitHub Actions CI/CD pipeline. This kind of account-level compromise is a reminder of why the JavaScript ecosystem's shift toward OIDC Trusted Publishers matters: cryptographic publish provenance makes it harder for an attacker to pass off a malicious package as legitimate.

A critical forensic signal: every legitimate axios 1.x release is published via npm's OIDC Trusted Publisher mechanism, cryptographically bound to a verified GitHub Actions workflow. The malicious 1.14.1 breaks that pattern entirely, no trustedPublisher, no gitHead, no corresponding Git commit or tag.

// axios@1.14.0, LEGITIMATE
"_npmUser": {
  "name": "GitHub Actions",
  "email": "npm-oidc-no-reply@github.com",
  "trustedPublisher": { "id": "github", ... }
}

// axios@1.14.1, MALICIOUS
"_npmUser": {
  "name": "jasonsaayman",
  "email": "ifstap@proton.me"
  // no trustedPublisher, no gitHead
}

There is no commit or tag in the axios GitHub repository corresponding to 1.14.1. The release exists only on npm.

Step 2: Staging a Phantom Dependency

Before publishing the backdoored axios versions, the attacker seeded a malicious package on npm: plain-crypto-js@4.2.1, published from a separate throwaway account. This package masqueraded as the legitimate crypto-js library, same description, same author attribution, same repository URL, but contained a postinstall hook whose only job was to run a dropper.

A grep across all 86 files in axios@1.14.1 confirms: plain-crypto-js is never imported or require()'d anywhere in the axios source code. It appears in package.json solely to trigger the postinstall hook. Zero lines of malicious code exist inside axios itself.

Step 3: Cross-Platform RAT Dropper

The postinstall script is a heavily obfuscated JavaScript file. All sensitive strings, C2 URL, shell commands, file paths, are XOR-encoded in an array and decoded at runtime using a key derived from the string "OrDeR_7077". The dropper branches into three OS-specific attack paths:

  • macOS: Downloads a RAT binary to /Library/Caches/com.apple.act.mond (designed to mimic an Apple system cache), makes it executable, and launches it silently via osascript
  • Windows: Copies PowerShell to %PROGRAMDATA%\wt.exe (disguised as Windows Terminal), drops a VBScript to fetch and execute a second-stage RAT
  • Linux: Targets CI/CD environments, fetches a Python-based RAT

Step 4: Self-Destruct

After deploying the second-stage payload, the dropper deletes itself and swaps in a clean package manifest:

// The attacker's own cleanup code embedded in the dropper
// Replaces malicious package.json with a clean decoy
rename("package.md", "package.json")  // clean stub overwrites evidence
unlink("setup.js")                    // dropper erases itself

A developer inspecting their node_modules after the fact sees no indication anything went wrong.

Indicators of Compromise (IOCs)

Malicious packages:

  • plain-crypto-js@4.2.1, malicious dependency, postinstall RAT dropper
  • axios@1.14.1, backdoored 1.x release
  • axios@0.30.4, backdoored 0.x legacy release

C2 infrastructure:

  • sfrclak.com:8000, command and control server (stage-2 delivery)
  • Path segments: /6202033, packages.npm.org/product0, packages.npm.org/product1, packages.npm.org/product2

macOS persistence:

  • /Library/Caches/com.apple.act.mond, RAT binary dropped on macOS

Windows persistence:

  • %PROGRAMDATA%\wt.exe, RAT disguised as Windows Terminal

What You Need to Do Right Now

  1. Check your node_modules for axios versions 1.14.1 and 0.30.4. If present, assume compromise.
  2. Downgrade immediately to axios@1.14.0 or axios@0.30.3, the last known clean versions.
  3. Audit your systems for IOCs: check for unexpected outbound connections to sfrclak.com:8000, unusual cron/scheduled tasks, or unknown binaries in /Library/Caches (macOS) or %PROGRAMDATA% (Windows).
  4. Rotate all secrets and credentials accessed from development machines that ran npm install during the window the malicious versions were live (~March 31, 00:21–03:15 UTC).
  5. Review your npm publish workflow: ensure your project uses OIDC Trusted Publishers so that manual token-based publishes break the signature chain.

Why This Attack Wasn'T Opportunistic

The operational sophistication here is notable:

  • The attacker seeded a clean decoy package (plain-crypto-js@4.2.0) 18 hours before the malicious release, establishing npm publishing history to avoid "zero-history package" alerts
  • Both the 1.x and 0.x release branches were hit 39 minutes apart to maximize coverage
  • Separate payloads were pre-built for three operating systems
  • The C2 URL uses path segments tied to the npm registry path structure, packages.npm.org/product0/1/2, making the exfiltration look like ordinary npm traffic
  • The dropper uses Apple's legitimate osascript binary on macOS, no malicious binary written to disk until after validation, making EDR detection harder

This is supply chain attack tradecraft at a level previously reserved for state-sponsored operations, now deployed against a top-10 npm package.


Sources: StepSecurity, npm registry metadata, axios GitHub repository. The axios security advisory is available on the npm security advisory database.

Frequently Asked Questions

Related articles

More coverage with overlapping topics and tags.

Fastify v5.9.0 Adds `request.mediaType` and `onMaxParamLength`, Hardens `forwarded` Header Trust, Chunks Large HTTP/2 Replies, and Moves Type Tests to TSTyche
security

Fastify v5.9.0 Adds `request.mediaType` and `onMaxParamLength`, Hardens `forwarded` Header Trust, Chunks Large HTTP/2 Replies, and Moves Type Tests to TSTyche

Fastify v5.9.0, published on 2026-06-28 (github.com/fastify/fastify), is the first minor release of the v5 line in 2026 and a substantial 65-PR cycle. The headline features are `request.mediaType` (a typed accessor for the negotiated media type, [#6653](https://github.com/fastify/fastify/pull/6653) by climba03003), `onMaxParamLength` route option ([#6716](https://github.com/fastify/fastify/pull/6716) by climba03003), and a security fix that no longer trusts `X-Forwarded-Host` and `X-Forwarded-Proto` when the incoming socket is missing ([#6684](https://github.com/fastify/fastify/pull/6684) by mcollina). The cycle ships an HTTP/2 buffer-chunking fix for large replies ([#6746](https://github.com/fastify/fastify/pull/6746) by mcollina), three schema-related performance wins (deferred `getSchemaSerializer` content-type parsing #6692, cached `ContentType` objects in `ContentTypeParser` #6694, `typeof` guard before `toString.call` in `send` / `onSendEnd` #6693 by aquie00t), Node.js 26 added to the test matrix ([#6728](https://github.com/fastify/fastify/pull/6728) by Fdawgs) and Node.js 20 dropped from the yarn CI matrix ([#6662](https://github.com/fastify/fastify/pull/6662) by Tony133), the migration of the type-test suite from hand-rolled `expect-type` to [TSTyche](https://github.com/mrazauskas/tstyche) ([#6532](https://github.com/fastify/fastify/pull/6532) by mrazauskas, with follow-ups #6726 and #6727), and a TypeScript-only fastify-plugin v6.0.0 bump. Other notable fixes: trailer `res.end` deduplication (#6676), trailer duplicate-completion guard (#6714), `error.code` on routing errors (#6678), `hasRequestDecorator` / `hasReplyDecorator` catching constructor-assigned built-ins (#6753), `getValidationFunction()` allowed to return `undefined` (#6665), and a socket `_meta` clear that closes a keep-alive leak (#6799).
Deno 2.9 Ships 1.98x Faster Cold Start, 2.2-3.1x Less RSS Under Load, Default-On npm Minimum Release Age, No-Downgrade Trust Policy, and Built-In Snapshot Testing
runtimes

Deno 2.9 Ships 1.98x Faster Cold Start, 2.2-3.1x Less RSS Under Load, Default-On npm Minimum Release Age, No-Downgrade Trust Policy, and Built-In Snapshot Testing

Deno 2.9 (Bartek Iwańczuk, published 2026-06-25 on deno.com/blog/v2.9) is the largest Deno release of the cycle. Cold start drops from 34.2 ms to 17.3 ms (1.98x), peak RSS on the Deno.serve realworld workload drops 2.2x (142 MB → 64 MB) and 3.1x on 1 MiB bodies (197 MB → 63 MB), and Deno.serve throughput climbs 1.27x realworld (56.8k → 72.4k req/s), 1.11x plaintext, and 1.18x on 1 MiB bodies. Supply chain hardening: npm minimum-release-age is enabled by default with a 24h window (PR #35458), and a new opt-in no-downgrade trust policy (PR #34927) refuses to resolve any version whose trust evidence (staged publish, trusted publishing, provenance attestation) is weaker than the strongest evidence on any earlier-published version of the same package. Test runner parity: built-in t.assertSnapshot() (#35139), Deno.test.each (#34938), --shard for CI fan-out (#35057), retry and repeats (#35053), change-aware --changed and --related (#35199), and coverage thresholds (#35056). Lockfile interop: deno install seeds deno.lock from package-lock.json, pnpm-lock.yaml, yarn.lock, or bun.lock (#34296, #35330, #35346, #35350, #35394), pnpm-workspace.yaml auto-migrates to deno.json / package.json (#34993), and git merge conflict markers in deno.lock auto-resolve (#34726). Plus: deno desktop graduates from experimental (the June 16 PR #33441), deno link / deno unlink / deno list / deno watch subcommands, stable --unsafe-proto (#34738), Web Locks API (#31166), Happy Eyeballs v2 (RFC 8305) (#31726), navigator.userAgentData (#34743), the WebCrypto Modern Algorithms proposal (ML-KEM, ML-DSA, SLH-DSA, ChaCha20-Poly1305, SHA-3 family, KMAC, Argon2) (#34447, #34448, #34914, #35223), Node 26.3.0 compat (#34746, #34747), Node-API v10 (#35270), and CSS module imports under --unstable-raw-imports (#35093). 165+ PRs land in this cycle.
Node.js 26.4.0 'Current' Ships node:vfs Subsystem (Matteo Collina), ESM Loader Package Maps (Maël Nison), TLS Certificate Compression, TCP_KEEPINTVL/TCP_KEEPCNT, and argon2 Stable
security

Node.js 26.4.0 'Current' Ships node:vfs Subsystem (Matteo Collina), ESM Loader Package Maps (Maël Nison), TLS Certificate Compression, TCP_KEEPINTVL/TCP_KEEPCNT, and argon2 Stable

Node.js 26.4.0 (Current), published 2026-06-24 by @aduh95, lands eight SEMVER-MINOR changes: a minimal node:vfs subsystem that mounts user-supplied virtual filesystems (PR #63115, Matteo Collina) plus a follow-up that dispatches node:fs/promises to mounted VFS instances (PR #63537), package maps for ESM loaders that route bare specifiers through the loader hooks (PR #62239, Maël Nison), TLS certificateCompression that wires RFC 8879 zlib and zstd compression through the OpenSSL build config (PR #62217, Tim Perry), TCP_KEEPINTVL and TCP_KEEPCNT support in net.Socket.setKeepAlive (PR #63825, Guy Bedford), caller-supplied buffers in fs.readFile / fs.readFileSync (PR #63634, Matteo Collina), closeIdleConnections that now also drops pre-request sockets (PR #63470, semimikoh), net.BlockList advanced to Release Candidate stability (PR #63050), and crypto argon2 + KEM encap/decap marked stable (PR #63924, Filip Skokan). The release also adds WebCrypto cSHAKE (PR #63988), QUIC listEndpoints (PR #63536) and X509Certificate handles (PR #63191), dgram connectSync / bindSync (PRs #63838 + #63932, Guy Bedford), early-TCP net.BoundSocket (PR #63951), an experimental fast FFI call path for AArch64 and x86_64 (PRs #63068 + #63941, Paolo Insogna), npm 11.17.0 (PR #63857), sqlite 3.53.2, and libffi 3.6.0.

Comments

Log in Log in to join the conversation.

No comments yet. Be the first to share your thoughts.