Prototype Pollution
You poison one shared object and suddenly every object in the app inherits a property you control.
What you will learn
- -Explain why __proto__ pollution affects every object at once
- -Find pollutable sinks in merge, clone, and query-parse code
- -Chain a polluted property into XSS or server-side RCE
Every plain object in one parent, Object.prototype. Change a property on that parent and every object in the process inherits it, including objects the app has not created yet. That is the whole bug. If you can reach Object.prototype through user input, you can inject a property that silently shows up on every lookup across the entire application.
The magic key is __proto__. When code walks an attacker-controlled path and hits that string, it is no longer setting a normal property, it is reaching up into the shared prototype. So a merge function that copies {"__proto__": {"isAdmin": true}} into a target does not create a property named __proto__. It writes isAdmin onto Object.prototype, and now someObject.isAdmin is true everywhere, unless that object explicitly set it otherwise.
Where it hides
The vulnerable shape is any code that recursively copies attacker-controlled keys into an object without checking for __proto__, constructor, or prototype. Deep-merge helpers are the usual suspect, and for years the small merge and defaultsDeep functions in older lodash versions were the poster children.
$lodash.merge(dst, JSON.parse(userBody))$Object.assign({}, deepClone(req.query))$qs.parse('a[__proto__][x]=1')That query-string case matters because it needs no at all. A like ?__proto__[isAdmin]=true gets parsed by libraries that support nested bracket notation into a nested object, and if the app merges that into its state, you polluted the prototype through a .
Confirm the pollution
Prove it in the console before you build an . Set a property through __proto__, then read it back off a brand new, unrelated object.
$let a = {}; a.__proto__.polluted = 'yes'$let b = {}; console.log(b.polluted)b is a fresh object that never touched your input, and it still reports polluted. That is the tell. In a real app you confirm the same way through the actual sink, sending the and then hitting an that reads a property you did not legitimately set.
Prototype pollution can flip flags, poison template rendering, and on backends reach command execution. That blast radius is the reason you confine this to your own apps or an engagement with written . Spin up a vulnerable Node app locally, or use the PortSwigger -side and -side prototype pollution labs, and pollute those.
Turning it into impact
Pollution by itself is just a weird shared property. The damage comes from a gadget, some existing code that reads a property, assumes it is safe because nobody set it, and does something dangerous with the value you supplied.
On the client, a common gadget is a library that reads an undefined config option and drops it into the . Pollute Object.prototype.src or an innerHTML sink and you get DOM out of a property the page never intended to expose.
On the server, the sharpest gadgets reach code execution. If the app later spawns a child process and the spawn options object is missing a field, a polluted default can inject one. The known Node chain pollutes properties like shell and NODE_OPTIONS so that the next child_process.spawn runs your command through a loaded --require file.
${"__proto__": {"argv0": "node", "shell": "node", "NODE_OPTIONS": "--require /proc/self/environ"}}Why does polluting through __proto__ affect objects the app has not even created yet?