Skip to content

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.

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.

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.

# via query/hash a script merges into a config object
https://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" => pollutable
# malicious JSON to an endpoint that does a recursive merge
POST /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.
__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.

Client XSS; on the server, authorization bypass, DoS, and, with the right gadget, full RCE.

  • Payloads with __proto__, constructor, prototype in JSON, query, or path.
  • Anomalous global behavior (properties appearing where they shouldn’t).

Log JSON bodies with dangerous keys; monitor dependency versions with known PP CVEs.

  • Reject/strip the keys __proto__, constructor, prototype before any merge.
  • Use prototype-less structures: Object.create(null) for data maps; Map instead of a plain object.
  • Object.freeze(Object.prototype) where feasible; --disable-proto=throw in Node.
  • Validate input against a strict schema (JSON Schema, Zod) that disallows extra keys.
  • Update merge libraries (lodash ≥ 4.17.12, etc.); use structuredClone instead of homegrown merges.
  • Avoid reading options “just in case” from the object without checking they’re own properties (hasOwnProperty).

Patch the merge function/dependency, audit which gadgets existed, rotate whatever the bypass could have touched.

  • 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).
  • Is there recursive merge/clone of input (query, JSON, nested form)?
  • Does ({}).__proto__.x get polluted after the request? (console/server test)
  • Does constructor.prototype work where __proto__ is filtered?
  • Client-side: does pollution reach an XSS sink (innerHTML, src)?
  • Server-side: can you pollute isAdmin/role and affect logic?
  • Are there gadgets toward templates or child_process (RCE)?
  • Are dangerous keys filtered before the merge?