Prototype Pollution - JavaScript-Objekt-Manipulation
Prototype pollution is a JavaScript-specific vulnerability in which attackers manipulate the prototype chain of objects. Since all JavaScript objects inherit from `Object.prototype`, controlled inputs in `__proto__` or `constructor.prototype` keys can override global object properties. Result: Denial of Service, property injection for privilege escalation, and often remote code execution in Node.js. Affected: lodash, jQuery (historically), all deepmerge/cloneDeep implementations without protection.
Prototype Pollution is a class of vulnerabilities that exploits JavaScript-specific quirks. Almost every JavaScript application has been vulnerable at some point—particularly due to popular libraries such as lodash (prior to 4.17.12), jQuery (prior to 3.4.0), and dozens of other npm packages. The vulnerability is subtle: normal code looks harmless until an attacker uses __proto__ as a key.
JavaScript Prototypes – The Basis of the Vulnerability
Every JavaScript object has an internal [[Prototype]]:
const obj = {};
obj.__proto__ === Object.prototype // true
Object.prototype.isPrototypeOf(obj) // true
Property Lookup Chain:
obj.toString()
// → obj itself: no toString → look in __proto__
// → Object.prototype: has toString! → found!
The Problem - Mutation of Object.prototype:
const maliciousInput = JSON.parse('{"__proto__": {"polluted": true}}');
// If this input flows into a merge function:
function merge(target, source) {
for (let key in source) {
target[key] = source[key]; // ← DANGEROUS without key validation
}
}
merge({}, maliciousInput);
// Now EVERY new object is poisoned:
const anyObj = {};
console.log(anyObj.polluted); // true ← PROTOTYPE POLLUTION!
Three attack vectors for __proto__ manipulation:
// 1. Direct __proto__ assignment:
obj["__proto__"]["isAdmin"] = true;
// 2. constructor.prototype:
obj["constructor"]["prototype"]["isAdmin"] = true;
// 3. __proto__ as a JSON key:
JSON.parse('{"__proto__": {"isAdmin": true}}')
// → Object.prototype.isAdmin = true → EVERY object now has isAdmin!
Exploitable Scenarios
1. Privilege Escalation (most common impact)
// Application code (simplified):
function checkAdmin(user) {
return user.isAdmin === true;
}
// Normal request:
const user = { name: "Alice" };
checkAdmin(user); // false (isAdmin not set)
// After Prototype Pollution:
// Attacker has passed {"__proto__": {"isAdmin": true}}
const user = { name: "Bob" }; // NEW object!
user.isAdmin // true ← via prototype chain!
checkAdmin(user); // true ← PRIVILEGE ESCALATION!
// Realistic example - RBAC bypass:
if (!req.user.roles.includes('admin')) { return 403; }
// After pollution: every user has roles via the prototype chain!
2. Remote Code Execution in Node.js (via Template Engines)
When prototype pollution is exploited on the server side, template engines such as EJS, Pug, or Handlebars can be affected: These use Object.prototype properties internally. Pollution of certain properties (outputFunctionName, escapeFunction) can lead to code execution when a template is rendered. Known exploit chains are documented for EJS, Pug, and Handlebars (CVE-2021-23358, CVE-2022-24999, and similar advisories).
3. DoS via Object Manipulation
Object.prototype.toString = null;
// EVERY JSON.stringify call throws a TypeError!
// → Denial of Service for the entire application
4. Prototype Pollution → XSS (Client-Side)
// jQuery < 3.4.0 (historical):
$.extend(true, {}, JSON.parse(userInput));
// __proto__ Pollution → jQuery internal properties compromised
// → XSS possible via event handler injection
Affected npm packages (historical)
Known vulnerable libraries (already patched!):
| Package | Affected function | CVE | Fix |
|---|---|---|---|
| lodash < 4.17.12 | _.merge(), _.defaultsDeep(), _.mergeWith() | CVE-2019-10744 (CVSS 9.1) | npm update lodash (>=4.17.12) |
| jQuery < 3.4.0 | $.extend(true, ...) with __proto__ input | CVE-2019-11358 | jQuery >= 3.4.0 |
| hoek < 5.0.3 (hapi.js) | hoek.merge() / hoek.clone() | CVE-2018-3728 | Update to >= 5.0.3 |
| flat < 5.0.1 | flat.unflatten({'__proto__.polluted': true}) | - | Update to >= 5.0.1 |
| minimist < 1.2.6 | CLI argument parser (widespread!) | CVE-2020-7598 | Update to >= 1.2.6 |
# Detection with npm audit:
npm audit --audit-level=high
# → Shows prototype pollution vulnerabilities in dependencies
# Detection with Snyk:
npx snyk test
# → Deeper analysis of the dependency chain
Detection in Pentest
Black-Box Test - JSON Input with __proto__
POST /api/settings HTTP/1.1
Content-Type: application/json
{"settings": {"__proto__": {"admin": true}}}
Alternative Syntax:
{"settings": {"constructor": {"prototype": {"admin": true}}}}
Deep merge payload:
{"a": {"b": {"c": {"__proto__": {"polluted": "yes"}}}}}
Pollution detection: GET /api/profile or GET /api/user - does the response contain new properties?
Parameter pollution in query strings
GET /api?__proto__[admin]=true
GET /api?constructor[prototype][admin]=true
Automated Scanning
- Burp Suite Intruder: test all JSON keys for
__proto__variants - PPMap / PPScan npm tools for automated black-box detection
Code Review (White-Box)
# Insecure merge patterns without key validation:
grep -r "Object.assign\|\.merge\|\.extend" src/
grep -r "for.*in.*source" src/ # for..in iterates over __proto__!
Mitigation Measures
1. Key validation before object assignment
// Insecure:
target[key] = source[key];
// Secure - exclude dangerous keys:
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
if (!FORBIDDEN_KEYS.has(key)) {
target[key] = source[key];
}
// Or with Object.hasOwn() (no prototype chain lookup):
if (Object.hasOwn(source, key)) {
target[key] = source[key];
}
2. Object.create(null) for dictionaries without a prototype
// Normal object: inherits from Object.prototype (vulnerable)
const obj = {};
// Null-prototype object: no prototype chain (safe!)
const safeMap = Object.create(null);
// safeMap.__proto__ → undefined
// Pollution not possible!
3. structuredClone() instead of manual merge (Node.js 17+)
// Unsafe (if merge has no key validation):
const config = customMerge({}, userInput);
// Safe (handles __proto__ correctly):
const config = structuredClone(userInput);
4. JSON Schema Validation before processing
import Ajv from 'ajv';
const ajv = new Ajv();
const schema = {
type: 'object',
properties: { name: { type: 'string' } },
additionalProperties: false, // ← Blocks __proto__ and others!
};
if (!ajv.validate(schema, input)) throw new Error('Invalid input');
5. Object.freeze(Object.prototype) (Emergency Hardening)
// Prevents mutation of Object.prototype:
Object.freeze(Object.prototype);
// All attempts at prototype pollution silently fail or result in a TypeError!
> Warning: May break legacy code → test first!
6. Keep dependencies up to date
- Run
npm auditregularly - Use Renovate/Dependabot for automatic updates
- Use Snyk in the CI/CD pipeline for dependency scanning
- Use
package.jsonoverrides for transitive dependencies with PP vulnerabilities