A Dormant npm Account Just Compromised 141 Mastra Packages in 88 Minutes
The @mastra scope was hijacked through a forgotten contributor account with stale publish access. The injected dependency scored 30 on Commit. The package it cloned scored 90. That gap was readable before the attack.
Between 01:12 and 02:36 UTC on June 17, 2026, a hijacked npm account published 141 malicious versions across the entire @mastra scope. The blast radius: roughly 8 million weekly downloads, including @mastra/core at 1M/week. The attack took 88 minutes.
The compromised account—ehindero—was a former contributor who published authentic alpha releases in late 2024, then went dormant. The credentials were stale. The scope access was not. npm does not expire publish permissions on inactivity.
The two-stage attack
The day before, a separate account (sergey2016) published easy-day-js@1.11.21—a clean clone of the legitimate dayjs library. Establishing credibility. Twenty hours later, easy-day-js@1.11.22 shipped with a postinstall hook that downloaded a second-stage payload, disabled TLS certificate validation, inventoried 166 cryptocurrency wallet browser extensions, extracted browser history, and installed platform-specific persistence: a LaunchAgent on macOS, a systemd user service on Linux, a registry Run key on Windows. Killing the process triggered reinstallation.
Then the hijacked ehindero account mass-published every @mastra/* package with easy-day-js injected as a dependency. npm’s caret resolution ensured ^1.11.21 pulled the weaponized 1.11.22.
What behavioral scoring shows
We scored both the clone and the original.
| Package | Score | Publishers | Downloads/wk | Age |
|---|---|---|---|---|
easy-day-js | 30 | 1 | 126 | <1 day |
dayjs | 90 | 1 | 58M | 8+ years |
Score 30 versus score 90. The 60-point gap is the distance between eight years of consistent releases and a package that existed for twenty hours. dayjs itself is flagged CRITICAL on Commit—sole publisher, 58 million weekly downloads—but it has the behavioral depth of a real project. easy-day-js has nothing. No release history. No download momentum. No community. Just a version number chosen to match.
npm audit: silent on both before the attack. No advisory to issue—neither package had a known vulnerability. The risk was structural.
What about @mastra/core itself?
| Package | Score | Publishers | Downloads/wk |
|---|---|---|---|
@mastra/core | 79 | 6 | 1M |
mastra | 72 | 8 | 431K |
create-mastra | 64 | 6 | 3K |
Commit did not flag @mastra/core as CRITICAL before the attack. Six publishers. Healthy score. The publisher concentration signal—which caught axios and chalk—was not triggered because Mastra had the diversity that should make a project safer.
And it would have been safer, if one of those six publishers hadn’t been dormant for eighteen months with credentials that still worked.
The dimension we don’t measure yet
Commit’s current model measures publisher count. The Mastra attack exploited publisher lifecycle. A contributor who published once in 2024 and never returned still had full scope access in June 2026. npm treats that the same as an active maintainer.
Snyk called it “project hygiene, not a zero-day.” They’re right. But hygiene that nobody measures is hygiene that nobody does.
The signal exists in the data. npm publish timestamps are public. The gap between a publisher’s last activity and their current scope access is computable. A 6-publisher project where 2 publishers have been dormant for 18 months is not the same as a 6-publisher project where all 6 shipped within the last quarter. Commit now measures it — same day. See the update below.
What would have helped
Three things, at different layers:
- CI gate on dependency changes. Running
npx proof-of-commitment --file package-lock.json --fail-on=riskyin CI would catch any newly-resolved dependency with a score below 40.easy-day-jsat 30 would have blocked the merge. Setup takes one line. - IDE hook for AI-assisted installs.
poc hookinterceptsnpm installcommands from Cursor, Claude Code, and Windsurf. If an AI coding assistant resolves a dependency that Commit scores as HIGH or CRITICAL, the install is blocked before it runs. Install the hook. - Publisher lifecycle scoring. Shipped same day — see update below. Every audit response now includes a
publisherLifecyclefield that flags dormant publishers with current scope access. The data to compute “dormant publisher with stale scope access” was already public; the model now reads it.
The Mastra team responded fast. Detection within two hours. Unpublished 110 packages. Deprecated the 6 they couldn’t unpublish. Revoked the compromised account. Required MFA for all future publishes. That’s good incident response.
But the 88-minute window was enough. If you ran npm install on any affected @mastra/* package between 01:12 and 05:15 UTC on June 17—rotate every credential on that machine.
Five security firms flagged the campaign within hours: Socket, JFrog, Snyk, StepSecurity, and Endor Labs. All five found the same root cause: a forgotten contributor account with never-revoked publish access.
Having six publishers didn’t prevent this. Having six active publishers would have.
Update — June 17, 20:30 UTC
Five hours and forty minutes after this post was published, publisher lifecycle scoring shipped. The “dimension we don’t measure yet” section above is now obsolete. Same day.
Re-running the audit against @mastra/core now returns a new field, publisherLifecycle, that cross-references per-version _npmUser data with the package’s current maintainers list. Two numbers matter: how many publishers went dormant and kept their scope access (dormantWithAccess), and how many went dormant after access was revoked (dormantRevoked). The first is a live attack surface. The second is closed.
| Package | Active | Dormant · access | Dormant · revoked | Active ratio |
|---|---|---|---|---|
@mastra/core | 1 | 3 | 4 | 12.5% |
mastra | 1 | 4 | 3 | 12.5% |
create-mastra | 1 | 1 | 0 | 50% |
ehindero, the account used in this attack, now reads hasCurrentAccess: false — the Mastra team revoked it during incident response. Good. But three other publishers on @mastra/core still hold scope access despite 16–20 months of inactivity: calcsam (20mo), smthomas (16mo), abhiaiyer (16mo). Same shape as ehindero a week ago. Same access path. None compromised yet, none verified-safe.
The new field is live behind every audit. Score the same package today and the JSON response includes a riskFlags entry: “WARN: 3 dormant publishers with current scope access — calcsam (20mo inactive), smthomas (16mo inactive), abhiaiyer (16mo inactive).” No new lockfile required. No setup. The signal was always in the data; the model now reads it.
Scores cited from Commit audit, run 2026-06-17. Incident timeline from Mastra’s incident report and Socket’s analysis. easy-day-js payload analysis from JFrog. Attribution comparison to Sapphire Sleet from Snyk.
Audit your dependencies: getcommit.dev/audit · Add a CI gate: one-line setup · Watch for changes: free API key