Prototype pollution is a JavaScript-specific vulnerability class where an attacker modifies the properties of Object.prototype — the base object that every JavaScript object inherits from — causing unintended behavior across an entire application. Because nearly all objects in JavaScript inherit from Object.prototype by default, polluting it with a single unsanitized merge, clone, or assignment operation can affect unrelated parts of a codebase that never touch the vulnerable line directly. The bug class has been responsible for critical CVEs in some of the most widely used packages in the npm ecosystem, including lodash (over 40 million weekly downloads), jQuery, and minimist, and has been chained into remote code execution in real production applications. It sits in the OWASP Top 10 as a subset of injection risk and remains common in Node.js backends, browser-side frameworks, and CI/CD tooling written in JavaScript or TypeScript.
What Is Prototype Pollution?
Prototype pollution is the unauthorized modification of Object.prototype (or another object's prototype) so that properties an attacker injects become visible on every object that inherits from it. In JavaScript, {} is shorthand for new Object(), and every plain object inherits properties like toString and hasOwnProperty from Object.prototype through the prototype chain. If application code recursively merges attacker-controlled input into an object without checking for keys like __proto__, constructor, or prototype, the attacker can write arbitrary properties onto the shared prototype. Once polluted, those properties appear on every object in the running process — including objects created by completely unrelated modules — until the process restarts.
How Does Prototype Pollution Actually Work?
Prototype pollution works by abusing recursive merge, clone, or "set nested property by path" utilities that walk attacker-supplied keys without excluding dunder properties. A classic payload looks like JSON.parse('{"__proto__":{"isAdmin":true}}') passed into a deep-merge function. If the merge function does target[key] = source[key] recursively and key equals __proto__, the assignment doesn't create a new property on target — it walks up the prototype chain and sets isAdmin on Object.prototype itself. From that point forward, ({}).isAdmin returns true everywhere in the process, including in authorization checks like if (user.isAdmin) that were never intended to be attacker-influenced. The same mechanism works through constructor.prototype when __proto__ is filtered but the constructor path is not.
What Real CVEs Show the Impact of Prototype Pollution?
Real CVEs show prototype pollution causing everything from denial of service to remote code execution in packages downloaded hundreds of millions of times a month. CVE-2019-10744, disclosed in July 2019, affected lodash before 4.17.12: its defaultsDeep and merge functions allowed __proto__ injection, and because lodash was (and remains) one of the most depended-upon packages in npm, the disclosure forced thousands of downstream projects to patch. CVE-2020-8203, disclosed weeks later, showed the same lodash flaw enabling prototype pollution through zipObjectDeep. CVE-2020-7598 hit minimist, a widely used CLI argument parser, where crafted --__proto__ flags polluted the prototype during argument parsing. CVE-2019-11358 affected jQuery's $.extend(true, {}, ...) deep-copy behavior in versions before 3.4.0, exposing millions of websites still running older jQuery bundles. CVE-2021-23337 went further: a prototype pollution flaw in lodash's template function could be escalated to command injection, giving attackers code execution rather than just data manipulation.
How Does Prototype Pollution Lead to Remote Code Execution?
Prototype pollution leads to remote code execution when the polluted property is later read in a "dangerous sink" — code that uses an object property to build a command, a template, or a file path. A well-documented example is Kibana's CVE-2019-7609, where prototype pollution in a Timelion visualization input let attackers inject a property that was later evaluated by a server-side JavaScript sandbox, resulting in full RCE on the Kibana server. Similarly, applications using child_process.exec with options objects built from user-influenced templates have been shown to execute attacker-controlled shell arguments once an attacker pollutes an env or shell property that the exec call reads without validation. The pattern is always the same two-stage exploit: (1) a "gadget" that lets attackers write to the prototype, followed by (2) a "sink" elsewhere in the same process that trusts a property value without verifying its origin — a gap that static analysis often misses because the write and the sink can live in entirely separate modules.
How Can Developers Detect Prototype Pollution in Code?
Developers can detect prototype pollution by combining static analysis that flags unsafe recursive merges with dynamic testing that actually sends __proto__ and constructor.prototype payloads at running endpoints. Tools like ESLint's no-prototype-builtins rule catch some unsafe patterns at write time, but they don't catch every custom merge helper, so teams also run targeted fuzzing with payloads such as {"__proto__":{"polluted":true}} and {"constructor":{"prototype":{"polluted":true}}} against JSON-accepting API endpoints, then check whether ({}).polluted becomes true in the running process. Software composition analysis (SCA) matters just as much here as custom code review: because most prototype pollution CVEs live inside third-party dependencies like lodash, minimist, and ajv (CVE-2020-15366), a dependency inventory that maps exact installed versions against the National Vulnerability Database is often the fastest way to find exposure — and the harder, more valuable question is whether the vulnerable function in that dependency is actually reachable from code the application calls.
How Can Teams Prevent Prototype Pollution?
Teams prevent prototype pollution by eliminating unsafe merge patterns and by hardening the objects most likely to be targeted. Concrete mitigations include: using Object.freeze(Object.prototype) at process startup to make the prototype immutable; replacing plain-object merges with Map for any structure that stores attacker-influenced keys, since Map has no prototype chain to pollute; using Object.create(null) to create objects with no inherited prototype for parsing untrusted JSON; upgrading to patched versions of lodash (4.17.21+), jQuery (3.5.0+), and minimist (1.2.6+) that filter __proto__ and constructor keys internally; and validating that any custom "set property by path" utility explicitly rejects __proto__, prototype, and constructor segments before assignment. Node.js 16.7+ and V8-based runtimes also support --disable-proto=throw, which throws an exception on any __proto__ access, giving teams a runtime kill switch for legacy code they can't immediately patch.
How Safeguard Helps
Safeguard's reachability analysis pinpoints which prototype-pollution CVEs in your dependency tree — lodash, minimist, ajv, or otherwise — sit in code paths your application actually calls, so teams stop burning triage time on vulnerable functions that are never invoked. Griffin AI correlates the vulnerable merge or clone function with downstream sinks across module boundaries, surfacing the two-stage gadget-plus-sink chains (like the Kibana Timelion pattern) that keyword-based scanners miss entirely. Continuous SBOM generation and ingest keep an accurate, versioned record of every JavaScript dependency in production, so newly disclosed prototype pollution CVEs are matched against your actual inventory within minutes of publication rather than during the next quarterly audit. When a fix is available, Safeguard opens an auto-fix pull request that bumps the affected package to a patched version and flags any custom merge utilities in your own code that match known-unsafe patterns, cutting remediation from a multi-day review cycle down to a single approval.