osbytes

Search

Find posts, projects, and members.

← back to blog

Mastra's entire @mastra npm scope got republished with an easy-day-js caret trap

2026-06-17by@osbytes7 min read
#security #supply-chain #npm #javascript #ai #incident-response

TL;DR

  • Between 01:01 and ~02:36 UTC, an attacker republished essentially the whole @mastra/* npm scope (143 packages per JFrog's analysis) with one injected production dependency: easy-day-js, a dayjs lookalike whose postinstall runs an obfuscated dropper (registry timing in the same write-up).
  • The trap is semver, not tarball surgery: packages pinned "easy-day-js": "^1.11.21" while 1.11.21 (published 2026-06-16) was clean and 1.11.22 (published 2026-06-17T01:01:33Z) added the payload. Fresh installs resolved to .22 automatically; scanning .21 alone misses the hit.
  • Mastra's incident report and remediation PR #18056 confirm the malicious dependency was injected at publish time; the GitHub source tree never contained easy-day-js. If you installed affected versions on a dev laptop or CI runner, treat the host as compromised until you hunt persistence and rotate secrets.

The payload hides one dependency level down

Mastra is a TypeScript AI-agent framework (Gatsby alumni, large @mastra scope on npm). Today's attack did not rewrite Mastra source in git. The attacker added a dependency line to already-published package manifests and repushed tarballs.

JFrog's teardown of @mastra/ai-sdk@1.4.6 shows how small the diff can look:

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

easy-day-js copies dayjs surface area: dayjs.min.js, plugin files, date-library keywords. Version 1.11.22 adds:

"scripts": {
  "postinstall": "node setup.cjs --no-warnings"
}

setup.cjs is a one-line obfuscated loader. Deobfuscated, JFrog reports it sets NODE_TLS_REJECT_UNAUTHORIZED=0, fetches a second-stage script from 23.254.164.92:8000, spawns it detached under a random hex filename in os.tmpdir(), then deletes itself. The stage-two payload beacons to 23.254.164.123:443, inventories wallet browser extensions, and can pull follow-on modules over the same channel. It also drops Node-themed persistence (~/Library/NodePackages/protocal.cjs on macOS, ~/.config/systemd/user/nvmconf.service on Linux, C:\ProgramData\NodePackages plus an HKCU Run key on Windows).

That is the same class of install-time execution we keep seeing in npm compromises, but the carrier here is a scoped framework with ~918K weekly downloads on @mastra/core alone (per JFrog's cited registry stats), not a one-off typosquat on a popular utility.

Why ^1.11.21 is worse than it reads

The sequencing is deliberate:

Artifact Timestamp (UTC)
easy-day-js@1.11.21 (clean decoy) 2026-06-16T07:05:42Z
easy-day-js@1.11.22 (weaponized) 2026-06-17T01:01:33Z
@mastra/ai-sdk@1.4.6 (example republish) 2026-06-17T01:27:27Z

The republished Mastra packages went up after the malicious easy-day-js version existed. Any install resolving ^1.11.21 on that day picked 1.11.22.

Teams auditing "what version did we pin?" or running scanners that only flag the explicitly named floor version can pass 1.11.21 while npm still installs .22. Lockfiles frozen before the attack are a separate question; greenfield npm install / pnpm install during the exposure window is the scary path.

First-party Mastra code review would not have caught this: the repo's package.json files and lockfiles stayed clean. PR #18056 states explicitly that easy-day-js was injected at publish time, verified absent from the source tree, and that remediation patch-bumps 131 publishable packages to roll latest forward on clean builds.

How access broke and what Mastra shipped

Mastra's incident report (posted 2026-06-17) says a current maintainer's npm token published 116 malicious packages between 6:12 and 6:37 PM PT on 2026-06-16 (the early-morning UTC wave JFrog timestamps). The maintainer was phished during a video call via a compromised LinkedIn outreach; Mastra compares the vector to other TypeScript maintainer reports the same week.

Operational notes from the same thread worth filing:

  • MFA was required on npm, but token bypass was mistakenly allowed; Mastra removed bypass across packages during response.
  • They unpublished or deprecated compromised versions, re-established org access where the attacker had locked them out, and merged the forward-roll changeset once containment (rotated tokens, removed unauthorized owners) was confirmed.

StepSecurity filed issue #18045 at 01:39 UTC, roughly when the publishing burst was still in flight.

What to run before you bump versions

Assume compromise if any @mastra/*, mastra, or create-mastra install ran during the exposure window (JFrog lists 143 affected package/version pairs; Mastra's own count drifted between 110–131 as unpublish work progressed). Uninstalling the package is not enough if postinstall already ran.

npm ls easy-day-js

Hunt loader artifacts JFrog documents:

  • <tmpdir>/.pkg_history, <tmpdir>/.pkg_logs, <tmpdir>/<24-hex-chars>.js
  • macOS: ~/Library/NodePackages/, ~/Library/LaunchAgents/com.nvm.protocal.plist
  • Linux: ~/.config/systemd/nvmconf/, ~/.config/systemd/user/nvmconf.service
  • Windows: C:\ProgramData\NodePackages, HKCU\...\Run value NvmProtocal

Rotate everything that touched the host: npm tokens, GitHub PATs, CI OIDC-backed secrets, cloud API keys, LLM provider keys, and any crypto wallet material on that machine. Mastra's clean versions are the forward-rolled releases from their remediation pipeline; verify latest on each package you depend on before trusting a blind semver bump.

For CI that does not need install hooks, ignore-scripts=true in the job's .npmrc remains the blunt instrument that would have blocked this class of payload (same lesson as the TanStack npm postmortem from May). It does not replace auditing who can still publish to your scope.

Sources