ua-parser-js: a billion-download npm package compromised in October 2021
In late October 2021, three versions of the npm package ua-parser-js — a user-agent parsing library with over six million weekly downloads — were published with malicious code that downloaded and executed a cryptocurrency miner alongside a credential-stealing payload. The package's author, Faisal Salman, lost control of his npm account briefly; during that window, the attacker pushed 0.7.29, 0.8.0, and 1.0.0.
This is a post-mortem of what happened, what made the attack effective, and how cooling-period and consensus-based defenses change the equation for packages like this.
What the malicious versions did
Each compromised version included a preinstall script that ran a small shell script bundled into the package. The script detected the operating system and downloaded one of two payloads:
- On Linux and macOS: an XMRig cryptocurrency miner.
- On Windows: a Trojan that searched for cookies, saved passwords, and other credentials in browser profiles, plus the same XMRig miner.
The miner was configured to throttle CPU usage on idle to make detection less obvious. The Windows payload exfiltrated data to an attacker-controlled server.
Why this attack mattered
ua-parser-js is not glamorous code. It parses user-agent strings into structured data. But it sits in the dependency graph of frameworks and analytics libraries used by tens of thousands of projects. Once an attacker pushes a malicious version, every CI pipeline that runs npm install without a lockfile, every npm install that resolves to the latest matching range, and every developer who pulls down a fresh checkout — all of them risk fetching the compromised tarball.
The attack window before npm pulled the malicious versions and the maintainer regained control was short — measured in hours — but the package's reach amplified the damage even within that window.
How the account was compromised
The maintainer publicly stated that the npm publishing account was taken over. Account takeovers on package registries usually trace to a small set of vectors: phishing for the maintainer's password, password reuse with credentials leaked from another service, or session-token theft. Two-factor authentication on the publishing account significantly raises the bar for any of these.
What standard defenses caught and missed
Threat feeds caught it eventually. Once researchers analyzed the new versions and reported them, the affected versions were yanked and CVEs were assigned. By then, an unknown number of installs had already happened.
Lockfiles helped. Projects that had pinned ua-parser-js to a specific known-good version in their lockfile and ran npm ci (which respects the lockfile exactly) were not exposed. Projects that ran npm install and resolved a fresh range were.
Provenance attestations didn't exist yet. npm provenance via Sigstore became generally available later. Provenance ties a published artifact to the source build; an attacker who lacks the build environment can't produce a valid attestation. For new packages or new versions today, provenance is a meaningful defense.
How a cooling-period gate would have caught it
Veln's cooling gate, configured with sensible defaults, refuses to install a package version that has been published less than two hours ago and has fewer than ten community observations. A version compromised at publish time falls under both conditions on its first install attempts: it is brand new and unobserved.
A developer or CI pipeline running with this gate sees a hold message instead of executing the malicious preinstall script. The hold doesn't accuse the package of being malicious — it simply refuses to be in the first wave of installs for a brand-new version.
For a package with the install volume of ua-parser-js, ten community observations accumulate within minutes of a legitimate release. The gate adds latency to legitimate releases but eliminates the window where a compromised version can run before anyone has analyzed it.
Takeaway
The ua-parser-js incident is not the largest npm attack by reach, but it is a clean illustration of the shape of the problem: a popular package, a brief account takeover, and a lifecycle script that runs unconditionally on install. Lockfiles plus a publication-age gate plus per-package community-observation thresholds remove most of the exposure window without requiring threat-feed coverage to be timely.