Tech 9 min read

Mastra npm compromise: easy-day-js postinstall RAT across 143–144 packages

IkesanContents

TL;DR

What happened On June 17, 2026 (UTC), Mastra’s @mastra/* npm packages were bulk re-published and execute an install-time payload through an added easy-day-js dependency. External research counts 143–144 packages; Mastra’s own incident report counts 116.

Where it fires Not in Mastra’s own code, but in the added "easy-day-js": "^1.11.21". A fresh install at the time resolved the malicious easy-day-js@1.11.22, whose postinstall setup.cjs fetches a second-stage payload and runs it in the background.

What to do Treat any dev machine, CI runner, or build environment that installed an affected version on June 17, 2026 (UTC) as compromised. Then:

  1. Check npm ls easy-day-js, your lockfile, your internal npm proxy, and your CI cache
  2. Rotate LLM API keys, cloud credentials, GitHub tokens, npm tokens, and anything wallet-related
  3. Assume the second-stage payload already ran even after you remove the dependency

Mastra’s @mastra/* scope had its legitimate packages re-published in bulk. The Hacker News, citing research from Endor Labs, JFrog, SafeDep, Socket, and StepSecurity, reported that 144 Mastra npm packages were compromised.

Mastra’s own incident report says 116 malicious npm packages were published from a hijacked maintainer token, of which 110 were unpublished and the remaining 6 deprecated. SafeDep counts 143, The Hacker News 144, and StepSecurity over 140. The numbers disagree because of versions removed right after publish, derived packages, and differences in when each party looked. When you check, match against the package names and versions left in your own lockfile, not the totals in any article.

Converting the official report’s timestamps to JST, the malicious publishes happened around June 17, 2026, 10:12–10:37. Mastra became aware around 12:45 the same day and unpublished or deprecated the affected packages by around 15:57. By around 17:00 it shipped safe new versions and removed token bypass (a setting that lets an npm token publish while skipping the MFA check) from every package. The compromised machine belonged to a current Mastra employee who clicked a suspicious link during social phishing over LinkedIn, per the official account.

The Mastra packages themselves don’t carry a large payload this time. The attacker added a single dependency line, easy-day-js, to package.json and offloaded the payload to that dependency’s postinstall.

Mastra’s own code doesn’t run, easy-day-js does

In JFrog’s analysis, the affected Mastra packages’ code is barely touched. What was added is a dependency like this:

{
  "dependencies": {
    "easy-day-js": "^1.11.21"
  }
}

easy-day-js is a typosquat (a fake package with a name made to resemble a real one) of the legitimate dayjs. On June 16, 2026, 07:05 UTC, a clean easy-day-js@1.11.21 was published first. Then on June 17, 2026, 01:01 UTC, easy-day-js@1.11.22 was published with a postinstall.

Mastra’s dependency range is ^1.11.21, so a fresh install at the time would also resolve 1.11.22. Reviewing only the Mastra diff makes it look like “one date-library-ish dependency got added,” but the actual install pulled in the malicious 1.11.22.

This pattern is close to the axios compromise where the fake dependency plain-crypto-js ran its postinstall. Reviewing the legitimate package never reaches the payload itself; what’s left is the added dependency and the version-range difference.

flowchart TD
    A["Attacker publishes easy-day-js 1.11.21"] --> B["Adds postinstall in 1.11.22"]
    B --> C["Bulk re-publishes @mastra/*<br/>with hijacked Mastra access"]
    C --> D["package.json gets<br/>easy-day-js ^1.11.21"]
    D --> E["Consumer fresh install<br/>resolves 1.11.22"]
    E --> F["setup.cjs fetches second-stage payload"]
    F --> G["Runs in background and self-deletes"]
    G --> H["Steals credentials and wallet data"]

Follow the process that survives after postinstall

The postinstall of easy-day-js@1.11.22 runs setup.cjs. In StepSecurity’s and SafeDep’s analyses, this loader disables TLS certificate verification, fetches a second-stage payload from 23.254.164[.]92, and runs it from a temp directory as a background Node.js process. The loader then self-deletes, thinning out traces inside the npm package.

Socket analyzes the second-stage payload as a cross-platform infostealer (information-stealing malware). It targets browser history, the stored data of 160+ crypto wallet extensions, persistence on Windows, macOS, and Linux, and exfiltration to the C2 server (the attacker’s command server) at 23.254.164[.]123. Because the C2 can return commands, it doesn’t stop at a one-shot file theft; it reaches fetching and running additional modules.

Mastra is used to build AI apps, agents, RAG, MCP, and cloud integrations. That means an affected machine often holds OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, cloud credentials, database connection strings, and GitHub and npm tokens. Whether you imported Mastra is not the line. The payload runs at npm install time.

In the Mini Shai-Hulud spread to @antv, a launch path remained on the Claude Code and VS Code side even after the dependency was reverted. The current public information doesn’t show the same .claude/ or .vscode/ persistence confirmed in the Mastra wave. But since the second-stage payload is reported to hold OS-level persistence, follow the machine’s autostart settings and running processes even after you delete node_modules.

It could be rejected as a publish without provenance

The difference SafeDep points to is SLSA provenance (proof of origin). Mastra’s legitimate releases ship from CI through npm’s trusted publisher flow (publishing to npm from CI’s OIDC), with a provenance attestation (the signature information proving origin) attached. The malicious versions this time were published from a personal token and dropped the provenance.

This runs opposite to the May Mini Shai-Hulud TanStack wave. In the TanStack/Mistral wave, the legitimate GitHub Actions path was poisoned and the malicious packages shipped with SLSA provenance attached. In the Mastra wave, publishes that weren’t from the legitimate CI path were mixed in, so there was room to reject them via a provenance-required policy or signature verification.

For a reader, the judgment splits like this:

CasePublish pathWhat provenance tells you
TanStack/Mistral waveLegitimate CI abusedProvenance is present. It says nothing about content safety
Mastra easy-day-js waveBulk publish from a personal tokenRejectable as a diff that isn’t from legitimate CI

Mastra’s official report also admits that while it had required MFA from npm maintainers, it had allowed token bypass. Even with MFA required, a publish using an existing token wasn’t stopped.

Where age gates and allowScripts stop it

In this timeline, a release-age gate (a setting that won’t install a version for a fixed period after publish) lands where it stops the attack.

easy-day-js@1.11.22 was published on June 17, 2026, 01:01 UTC, and Mastra’s bulk publish began 11 minutes later. If pnpm 11’s minimumReleaseAge or Yarn 4.10’s npmMinimalAgeGate is enabled with a one-day wait, a fresh install stops before it resolves easy-day-js@1.11.22. This is exactly the “wait out short-exposure npm poisoning” pattern I wrote about in the pnpm 11 and Yarn 4.10 age-gate article.

But an age gate won’t clear a malicious version that’s already aged past the window, or an install after the version was pulled into your internal proxy. Once it’s in the lockfile, later CI keeps using the same version. Comb through what’s left in your internal npm mirror, Docker layers, GitHub Actions cache, and pnpm store one by one.

The other boundary is install scripts. As seen in the npm v12 allowScripts article, npm v12 is set to block unapproved preinstall, install, and postinstall by default. The easy-day-js here is a postinstall, so if it’s unapproved it stops before the payload launches.

These two play different roles. An age gate is “don’t fetch it,” allowScripts is “don’t run it even if you fetched it.” In the Mastra wave, both land on the same spot.

Start from the lockfile, not the Mastra version name

The affected case is an environment that freshly installed @mastra/* on June 17, 2026 (UTC). Mastra officially states the malicious publishes happened around June 17, 2026, 10:12–10:37 JST. External research looks more broadly at 01:12–02:39 UTC, so in JST suspect June 17, 2026, 10:12–11:39 first.

Checking the npm registry on June 18, 2026 JST, easy-day-js@1.11.22 is gone from the metadata and latest is back to 1.11.21. 1.11.22 not showing up in a current npm view easy-day-js is not proof you didn’t install it at the time. Look at whether the version or tarball from then is left in your lockfile, internal npm proxy, or CI cache.

The first thing to look at is the lockfile.

npm ls easy-day-js
rg '"easy-day-js"|easy-day-js@1\.11\.22|@mastra/' package-lock.json pnpm-lock.yaml yarn.lock

Treat any machine or CI runner where easy-day-js@1.11.22 shows up as compromised, regardless of whether it was imported. Even if only easy-day-js@1.11.21 shows up, cross-check the version and install time of the relevant @mastra/*. Even if you remove the dependency with npm uninstall, treat it on the assumption that the second-stage payload already ran. Cross-check the process list, temp directories, the OS autostart settings, outbound traffic, reachability to the C2 addresses 23.254.164[.]92 and 23.254.164[.]123, and any suspicious Node.js child processes.

Match the rotation scope to what Mastra touches. If you used LLM API keys, GitHub tokens, npm tokens, AWS, GCP, Azure, Vault, Kubernetes, database connection strings, or wallet extensions on the same machine, treat those values as exposed too. If a CI runner was hit, check the per-job environment variables plus the long-lived credentials and caches left on the runner host.

References