Prototype Pollution
In JavaScript almost every object inherits from Object.prototype. If an application merges attacker-controlled data into an object without filtering keys like __proto__, constructor, or prototype, the attacker can write to the global prototype and thereby inject properties that show up on every object in the process. That turns an innocent merge into flow control: from XSS to RCE in Node.
Threat model
Section titled “Threat model”The bug is an unsanitized property write that escalates to a global property. The attacker doesn’t modify one object: they pollute the base class everything inherits from. The dangerous part is the “gadget”: some legitimate code reading a property that normally doesn’t exist (options.isAdmin, config.shell, a template option) and now finds it because it’s on the prototype.
Where it arises
Section titled “Where it arises”Functions that recursively merge/clone/parse objects without blocking dangerous keys: merge, extend, clone, defaultsDeep (old lodash), nested query-string parsing, JSON.parse + merge, deserializers.
Red Team
Section titled “Red Team”Client-side (browser) → XSS/DOM
Section titled “Client-side (browser) → XSS/DOM”# via query/hash a script merges into a config objecthttps://target/#__proto__[html]=<img src=x onerror=alert(1)>https://target/?__proto__[innerHTML]=...If a library (old jQuery, sanitizers, templating) reads a polluted option, you get XSS. Detection: in the console, after load, check Object.prototype.polluted.
# manual console test({}).__proto__.test = "pwned";({}).test; // -> "pwned" => pollutableServer-side (Node) → DoS / bypass / RCE
Section titled “Server-side (Node) → DoS / bypass / RCE”# malicious JSON to an endpoint that does a recursive mergePOST /api/profile{"__proto__":{"isAdmin":true}}- Auth/logic bypass: pollute
isAdmin,role, flags later read by default. - DoS: pollute properties that break the runtime.
- RCE: known gadgets — pollute options ending up in
child_process.spawn(shell,NODE_OPTIONS,env), or in template engines (EJS, Pug, Handlebars) whose compiled options are read from the prototype → code execution. RCE depends on finding the right gadget in the dependencies.
Keys to try
Section titled “Keys to try”__proto__ constructor.prototype obj[__proto__][x]constructor ?__proto__[x]=y {"constructor":{"prototype":{"x":"y"}}}__proto__ is often filtered but constructor.prototype sometimes isn’t.
- PPScan / ppmap — automated client-side detection.
- Burp Suite — prototype pollution extension (server-side) to probe gadgets.
- DOM Invader (Burp) — detects PP sources/sinks in the browser.
Impact
Section titled “Impact”Client XSS; on the server, authorization bypass, DoS, and, with the right gadget, full RCE.
Blue Team
Section titled “Blue Team”Detection
Section titled “Detection”- Payloads with
__proto__,constructor,prototypein JSON, query, or path. - Anomalous global behavior (properties appearing where they shouldn’t).
Telemetry
Section titled “Telemetry”Log JSON bodies with dangerous keys; monitor dependency versions with known PP CVEs.
Hardening
Section titled “Hardening”- Reject/strip the keys
__proto__,constructor,prototypebefore any merge. - Use prototype-less structures:
Object.create(null)for data maps;Mapinstead of a plain object. Object.freeze(Object.prototype)where feasible;--disable-proto=throwin Node.- Validate input against a strict schema (JSON Schema, Zod) that disallows extra keys.
- Update merge libraries (lodash ≥ 4.17.12, etc.); use
structuredCloneinstead of homegrown merges. - Avoid reading options “just in case” from the object without checking they’re own properties (
hasOwnProperty).
Response
Section titled “Response”Patch the merge function/dependency, audit which gadgets existed, rotate whatever the bypass could have touched.
CVEs and real-world cases
Section titled “CVEs and real-world cases”- CVE-2019-10744 (lodash
defaultsDeep) — massive prototype pollution; a canonical reference. - CVE-2018-3721 / CVE-2020-8203 (lodash) — more cases in
merge/set. - CVE-2019-11358 (jQuery) — prototype pollution in
jQuery.extend(true, …)(before 3.4.0); affected countless sites. - Kibana / various Express apps — PP→RCE chains documented by researchers (Michał Bentkowski, PortSwigger).
Testing checklist
Section titled “Testing checklist”- Is there recursive merge/clone of input (query, JSON, nested form)?
- Does
({}).__proto__.xget polluted after the request? (console/server test) - Does
constructor.prototypework where__proto__is filtered? - Client-side: does pollution reach an XSS sink (innerHTML, src)?
- Server-side: can you pollute
isAdmin/roleand affect logic? - Are there gadgets toward templates or
child_process(RCE)? - Are dangerous keys filtered before the merge?