Safeguard
Open Source Security

Top open-source vulnerabilities in the npm ecosystem

Sonatype logged 512,000+ malicious npm packages in a year — a 156% jump. Here are the recurring vulnerability classes and how to catch them in CI.

Safeguard Research Team
Research
Updated 7 min read

Prototype pollution does not exist as a vulnerability class in Python, Java, or Go — it is a defect specific to how JavaScript resolves object property lookups through the prototype chain, and it has produced some of the ecosystem's most persistent CVEs. Lodash alone shipped two: CVE-2018-3721 in _.merge and CVE-2019-10744 in _.defaultsDeep. jQuery's $.extend() carried CVE-2019-11358. minimist, a tiny, dependency-free argument parser pulled into hundreds of thousands of projects as a transitive dependency, carried CVE-2020-7598. These aren't obscure edge cases — they sit in packages nearly every Node.js build depends on somewhere in its tree. Layer on top of that a different threat model entirely: npm's registry has been the target of some of open source's most consequential account-takeover attacks, from the 2018 event-stream incident to the 2021 ua-parser-js compromise that CISA issued an alert over. Sonatype's 10th annual State of the Software Supply Chain report, published in 2024, counted a 156% year-over-year surge in open-source malware and more than 512,000 malicious packages logged across public repositories in a single year. This post breaks down the vulnerability classes actually driving those numbers — many of them JavaScript vulnerabilities with no real equivalent in other ecosystems — and what catching them in CI requires beyond a CVE database lookup.

What makes prototype pollution a uniquely JavaScript vulnerability class?

Prototype pollution happens because JavaScript objects inherit properties through a shared prototype chain, so writing to a key like __proto__, constructor, or prototype on one object can silently add that property to every object in the running process. A merge or deep-clone utility that recursively copies keys from untrusted input — without checking whether a key name is one of those three — lets an attacker inject arbitrary properties onto Object.prototype. Lodash's _.merge (CVE-2018-3721) and _.defaultsDeep (CVE-2019-10744) both had this flaw, as did minimist's argument-parsing logic (CVE-2020-7598) and jQuery's $.extend() (CVE-2019-11358). The impact varies by application: it can flip an authorization flag an app never expected to be attacker-controlled, or — when the polluted property later flows into a template engine or a require() call — escalate to remote code execution. Because the vulnerable pattern (recursive merge of unsanitized keys) recurs in dozens of small utility packages, prototype pollution keeps reappearing in new libraries years after the original disclosures.

How did a template-rendering bug in lodash go undetected for years?

CVE-2021-23337 lived in _.template(), lodash's client-side templating function, which could be coerced into executing attacker-controlled JavaScript through crafted template options — effectively a command-injection path — in every version prior to 4.17.21. It was patched via GitHub Security Advisory GHSA-r5fr-rjxr-66jc. The bug persisted because _.template() is rarely the function developers think to audit; teams scrutinize eval() and Function() constructors but treat a well-known utility library's templating helper as safe by default. A related, equally recurring class is ReDoS — regular expressions with catastrophic backtracking that hang a process on a crafted input string. CVE-2021-27292 in ua-parser-js, a library used to parse browser user-agent strings on nearly every request path of countless Node.js services, is a textbook example: a single malicious User-Agent header could pin a CPU core indefinitely. Both bug classes share a root cause — they live inside dependency code most teams never read, and pattern-matching linters rarely flag regex complexity or template-engine misuse as a security concern. Neither shows up on a generic list of JavaScript vulnerabilities pulled from a CVE feed unless the scanner actually resolves the transitive dependency tree.

What happened when attackers compromised event-stream and ua-parser-js?

Both incidents show that the biggest npm risks don't always come with a CVE attached. In 2018, an attacker gained publish access to event-stream and added a malicious dependency, flatmap-stream, that specifically targeted users of the Copay Bitcoin wallet; Snyk and the npm security team traced roughly 8 million downloads over two and a half months before the payload was discovered and pulled. In October 2021, ua-parser-js — a library pulled by roughly 6-8 million weekly downloads at the time — had three versions (0.7.29, 0.8.0, and 1.0.0) compromised through a takeover of the maintainer's npm account; the malicious releases dropped XMRig cryptominers and credential-stealing trojans on Windows and Linux systems, prompting a CISA alert before fixed versions 0.7.30, 0.8.1, and 1.0.1 shipped. Neither attack required a code vulnerability at all — both rode a legitimate maintainer's publish credentials straight into millions of production installs, which is precisely the scenario a CVE-matching scanner is structurally unable to catch.

How do typosquatting and postinstall scripts turn npm install into an attack?

npm's install lifecycle runs arbitrary code by design: a postinstall script in package.json executes automatically the moment a package is pulled down, no explicit user action required beyond running npm install. Attackers exploit that in two overlapping ways. First, typosquatting — publishing a package with a name one keystroke off from a popular one (a missing hyphen, a transposed letter) — banks on developers or automated tooling installing the wrong name. Second, once installed, a malicious postinstall hook can read ~/.npmrc for registry auth tokens, scan for AWS credentials in ~/.aws, or fetch and execute a second-stage payload from a remote host — all before the developer has written a single line of code that imports the package. Because the payload runs at install time rather than at import time, static analysis of your own application code never even encounters it; the compromise is complete before your code review process has anything to review.

How much npm malware is actually out there?

More than most teams assume, and the trend is accelerating. Sonatype's 10th annual State of the Software Supply Chain report (2024) recorded a 156% year-over-year increase in open-source malware and logged over 512,000 malicious packages across public repositories in the preceding year, with roughly 17% rated critical severity. That volume outpaces what any manual review process or CVE feed can keep current — most of those packages never receive a CVE identifier at all, since CVE assignment presumes a vulnerability report and remediation timeline that malicious-by-design packages simply skip. For teams relying solely on npm audit or a CVE-database SCA scan, this is a structural blind spot: the fastest-growing category of npm risk is largely invisible to the tool most commonly used to manage it.

How Safeguard Helps

Catching prototype pollution and account-takeover malware in CI requires going past CVE-database matching in two directions at once: deeper into the dependency tree, and into the behavior of the artifact itself. Safeguard's deep dependency scanning walks the resolved npm graph to depth 100 — roughly 40 levels past the industry norm of 50–60 — because packages like minimist and lodash are rarely direct dependencies; they show up three or four levels down, pulled in by a build tool or an internal wrapper package, and shallow scans miss them entirely. Because event-stream and ua-parser-js prove that the riskiest npm publishes never carry a CVE, Safeguard's Eagle classifier scores every npm publish — plus a ten-year retroactive scan of the existing registry — across install-script behavior, egress patterns, credential-harvesting signals, and typosquat similarity, with a reported false-positive rate near 1 in 10,000 at the malicious threshold. Wired into CI as a fail-on: malicious gate, that combination catches both the known CWE-1321 prototype-pollution pattern in a transitive dependency and the unknown, CVE-less compromise that only behavioral analysis of the published artifact can surface.

Never miss an update

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