Safeguard
Supply Chain Attacks

The eslint-config-prettier npm compromise: when phishing beats your SCA scanner

A phishing email spoofing npm support hijacked a maintainer's account and poisoned eslint-config-prettier, a package with roughly 30 million weekly downloads.

Safeguard Research Team
Research
Updated 6 min read

On July 18, 2025, at 15:51 GMT, a malicious version of eslint-config-prettier appeared on the npm registry. The package, which formats ESLint output alongside Prettier and pulls roughly 30 million downloads a week, is a dependency almost every JavaScript project touches indirectly. Registry maintainers and the security community caught it fast — every poisoned version was pulled by 18:40 GMT the same day, a window of under three hours — but not before versions 8.10.1, 9.1.1, 10.1.6, and 10.1.7 shipped with a postinstall script that dropped a Windows PE32+ binary onto infected machines. The root cause tracked as CVE-2025-54313 wasn't a bug in the code at all: it was a phishing email, spoofing npm's own support address and referencing a real support ticket for credibility, that pointed the package's maintainer to a typosquatted lookalike of npmjs.com and harvested credentials there. Because the same npm account also published eslint-plugin-prettier, synckit, @pkgr/core, and napi-postinstall, the attacker's reach extended to a combined weekly download footprint researchers estimated near 78 million. This incident is a clean case study in why static code review and dependency-vulnerability scanning alone cannot stop a compromised human being with a valid publish token.

What actually happened in the eslint-config-prettier compromise?

An attacker sent a phishing email impersonating npm's support team, spoofing an address resembling support@npmjs.org and citing a real-looking ticket number to appear legitimate. The email linked to npnjs.com — a single-character typosquat of npmjs.com — where the maintainer, publishing under the handle JounQin, entered credentials that were harvested and used to mint or steal a valid publish token. With that token, the attacker pushed new versions of eslint-config-prettier directly to the registry without touching GitHub, opening no pull request, and triggering no code review. Researchers at Socket, Aikido, and Step Security independently caught the anomaly within hours because the published npm package no longer matched its GitHub source — a detectable but easily missed signal outside of automated tooling. The malicious payload lived in a postinstall script that ran automatically on npm install, before any test suite or CI gate had a chance to inspect it.

Why does maintainer-account compromise bypass code review entirely?

Code review protects the path from a pull request to a merged commit. It says nothing about the separate path from a maintainer's laptop to the npm registry — and neither does a linting rule set. A tool like eslint-plugin-security is built to flag risky patterns in your own source (unsafe RegExp construction, child_process calls with unsanitized input), not to detect a malicious postinstall script injected into a dependency's published tarball after the fact; it runs on code your team wrote, not on the artifact npm actually serves. When an attacker has a valid publish credential, they can push a tarball straight to npm that never corresponds to any commit in the project's public GitHub history — which is exactly what happened here. Endor Labs' writeup on CVE-2025-54313 and ReversingLabs' analysis both note that diffing the published npm tarball against the tagged GitHub release would have surfaced the discrepancy immediately, but almost no consuming project does that check by default. This is the structural weakness of the whole npm trust model: npm install trusts whatever the registry currently serves under a package name and version, not a specific reviewed commit. A thousand approving code reviewers upstream do nothing if the artifact your CI actually pulls was substituted afterward.

What did the malicious payload actually do?

The postinstall script, present in the four confirmed malicious eslint-config-prettier versions plus poisoned releases of eslint-plugin-prettier, synckit, @pkgr/core, and napi-postinstall, dropped a Windows-only PE32+ binary named node-gyp.dll onto the infected host and launched it via rundll32.exe. Security researchers named the payload family "Scavenger," after recurring "SCVNGR" strings found in its variants, and identified it as functioning as an information stealer and remote-access tool capable of harvesting credentials and system data. Critically, the payload path was Windows-specific: installs on macOS or Linux triggered the postinstall script but did not execute the PE32+ binary, meaning the practical blast radius was concentrated on Windows developer and CI machines that ran npm install inside the roughly three-hour exposure window before the registry pulled the versions.

Why do phishing attacks against maintainers work so well?

Phishing succeeds against maintainers for the same reason it succeeds against everyone else — it targets attention and trust under time pressure — but the stakes are asymmetric. A phished employee typically compromises one company's systems; a phished open-source maintainer with a heavily-depended-on package compromises every downstream consumer simultaneously. The npnjs.com domain in this attack is a single dropped letter from npmjs.com, a typosquat cheap enough to register and convincing enough to defeat a quick glance, especially when paired with a legitimate-looking support ticket reference. Snyk, CSO Online, and BleepingComputer's independent coverage all converge on the same conclusion: this was not a sophisticated zero-day or a supply-chain build-system compromise like SolarWinds — it was a well-crafted but conventional credential-phishing email that happened to land on an account with outsized downstream reach. That asymmetry is precisely why npm maintainer accounts for high-download packages deserve materially stronger account-security requirements than an average developer's login.

What would have stopped this attack before it reached a developer's machine?

Two independent controls line up almost exactly with this incident's mechanics. First, npm's own two-factor authentication and token-scoping features — if enforced with a hardware-backed or phishing-resistant second factor rather than a factor an attacker's harvested session could replay — narrow the window in which a stolen password alone yields a valid publish token. Second, and more directly relevant for defenders who don't control the maintainer's account, install-time inspection of the artifact itself catches exactly this failure mode regardless of how the credential was obtained. A postinstall script that writes an unexpected binary to disk, or reaches out to fetch and execute additional payloads, is a behavioral signal that exists independent of whether the underlying code was ever reviewed. That is the layer that matters once you accept that some maintainer, somewhere in your dependency tree, will eventually be phished.

How does Safeguard address this attack pattern?

Safeguard's Package Firewall sits as an install-time proxy in front of npm and pip, so every fetch — including transitive dependencies your own manifest never names directly — is evaluated before it reaches a developer machine or CI runner. A behavioral analyzer inspects each package archive statically, without executing it, which is exactly the check that would have flagged an eslint-config-prettier release shipping a new postinstall script dropping an unexpected Windows binary. Eagle, Safeguard's malware classification model, scores install-script behavior, egress patterns, and behavior divergence from a package's prior versions as distinct indicator classes — a previously benign package suddenly writing outside its own directory or contacting new infrastructure is precisely the divergence signal Eagle is built to catch. Because Eagle retroactively re-scores its entire historical corpus whenever it's updated, teams that installed one of the four malicious versions during the exposure window would surface a finding the moment the classification model learned the new indicator, rather than waiting to notice on their own that a routine formatting dependency had quietly reached out to install a stealer.

Never miss an update

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