Safeguard
Supply Chain Attacks

Anatomy of an npm maintainer account takeover

A single phishing email hit eslint-config-prettier's ~30M weekly downloads in July 2025 — no code compromise needed, just a stolen npm login.

Safeguard Research Team
Research
6 min read

On July 19, 2025, attackers registered a lookalike domain, npnjs.com, and used it to send spear-phishing emails to npm package maintainers asking them to "verify their credentials." No zero-day, no dependency confusion, no supply-chain code injection — just a fake login page that harvested npm registry passwords and tokens. Within hours, the stolen credentials let attackers log in as legitimate maintainers and publish malicious versions of eslint-config-prettier (roughly 30 million weekly downloads), eslint-plugin-prettier, synckit, @pkgr/core, napi-postinstall, got-fetch, and two versions of the is package, shipping Windows-targeted malware inside otherwise-trusted packages. Snyk's research team, which documented the incident, called out a pattern worth naming on its own: one compromised maintainer account becomes a trusted platform for phishing the next maintainer in the same dependency graph — a chain reaction, not an isolated breach. The is package was remediated in roughly six hours; other packages took longer to fully clean up. This kind of attack bypasses nearly every control teams build around code review, because the malicious code is published by an account with full legitimate signing rights, into a package version number that looks like any other release. This post breaks down how these takeovers actually work and what stops them.

Why do attackers target maintainer credentials instead of code?

Attackers target credentials because compromising an npm account skips every review gate a legitimate release goes through. A malicious pull request has to pass code review, CI, and a maintainer's own judgment before merge. A credential-based takeover skips all of that — the attacker simply runs npm publish as the real maintainer, and the registry has no way to distinguish that from a routine release. This is structurally different from typosquatting or dependency confusion, where the attacker has to trick a build system into resolving the wrong package name. Here the package name, the maintainer identity, and the cryptographic publish rights are all genuinely legitimate — only the person typing the command has changed. Phishing is also cheap and scalable: a single templated email sent to hundreds of maintainers only needs to work once to yield a package with tens of millions of weekly downloads, as the eslint-config-prettier incident showed.

How did the npnjs.com phishing campaign actually work?

The campaign relied on a single-character typosquat of the npm domain — npnjs.com in place of npmjs.com — paired with an urgency-driven pretext: maintainers received email asking them to "verify" their account or risk losing publish access. Clicking through led to a fake login page that captured the maintainer's registry credentials in real time. Per Snyk's writeup, the attackers didn't need to also defeat two-factor authentication through any documented bypass technique; the source didn't specify one, which itself points at a gap many maintainers still have — accounts without 2FA enabled at all, or with a token-based publish flow that doesn't prompt for a second factor on every npm publish call. Once inside, the attackers pushed new versions of each affected package containing a Windows-targeted payload, using the exact same version-bump mechanics a legitimate maintainer would use, which is why registry-level automated scanning after publish — not just pre-merge code review — became the thing that actually caught it.

What made this a "virtual infection" rather than a single breach?

Snyk's researchers used the term "virtual infection" because the attack didn't stop at one compromised account — each successful takeover gave attackers a foothold to target maintainers in adjacent, related projects with the same phishing playbook, since ecosystem maintainers frequently collaborate, share contact lists, or co-maintain multiple packages. Compromising eslint-config-prettier's maintainer, for instance, put the attacker in a position of apparent legitimacy relative to the broader Prettier and ESLint tooling ecosystem, making a follow-on phishing attempt against a co-maintainer of synckit or @pkgr/core more credible than a cold email would be. This is the same trust-chaining dynamic seen in other ecosystem-wide incidents: a single seed compromise multiplies because open-source maintainers operate inside real social and technical trust networks, and an attacker who has already breached one node can borrow that trust to reach the next.

What actually stopped the damage, and how fast?

Community response, not registry-level prevention, is what limited the blast radius. Once affected maintainers and researchers identified the malicious versions, the response was to reset npm tokens for the compromised accounts and deprecate or pull the malicious package versions from the registry. The is package saw resolution in roughly six hours from when the incident was flagged. That speed is a genuine win — but six hours is also more than enough time for npm install to run across thousands of CI pipelines and developer machines worldwide, given how frequently popular linting and build-tooling packages get pulled fresh. The incident is a clean illustration of a structural gap: npm's registry has no automated step that classifies a newly published version's behavior before it becomes installable, so detection depends on a human — a maintainer, a researcher, a downstream user — noticing something wrong and raising the alarm.

What should maintainers and consuming teams actually do about this?

Snyk's own recommendations from the incident are a reasonable floor: enable 2FA on every npm publishing account, vet new dependencies with a tool like npq before installing, and set ignore-scripts=true in .npmrc so a compromised package's postinstall hook can't execute automatically on npm install. Beyond that baseline, consuming teams should treat "maintainer reputation" as a moving target, not a one-time trust decision — a package that's been safe for years can flip the moment its publish credentials are stolen. That means the highest-leverage defense sits downstream of the maintainer entirely: scanning every new package version's actual behavior — install scripts, network calls, credential-file access — at the moment it's published, rather than trusting it because a known maintainer's name is attached to the release.

How Safeguard helps

Safeguard's Eagle malware-classification model scores every npm publish — not just new packages, but every version bump from an existing, previously-trusted maintainer — across indicators including install-script behavior, egress patterns to unusual hosts, and credential-harvesting reads from paths like ~/.npmrc and ~/.ssh, the exact profile a Windows-targeted payload published through a hijacked account would exhibit. Because Eagle re-scores retroactively when its model improves, a malicious version that slips through at publish time can still surface as a finding later, with a recommended rollback or Gold Registry substitution, rather than depending on a maintainer or researcher happening to notice first. That closes real time on incidents like this one, where the gap between publish and detection is precisely where the damage compounds. Maintainer credential hygiene — 2FA, token rotation, phishing awareness — remains the responsibility of the humans holding publish rights; Safeguard's job is to make sure that if that layer fails anyway, the malicious code doesn't get a multi-hour head start into your build pipeline.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.