Prototype Pollution - JavaScript-Objekt-Manipulation
Prototype Pollution ist eine JavaScript-spezifische Schwachstelle bei der Angreifer die Prototyp-Kette von Objekten manipulieren. Da alle JavaScript-Objekte von Object.prototype erben, können kontrollierte Eingaben in __proto__- oder constructor.prototype-Schlüsseln globale Objekteigenschaften überschreiben. Resultat: Denial of Service, Property-Injection für Privilege Escalation, in Node.js häufig Remote Code Execution. Betroffen: lodash, jQuery (historisch), alle deepmerge/cloneDeep-Implementierungen ohne Schutz.
Prototype Pollution ist eine Schwachstellenklasse die JavaScript-spezifische Eigenheiten ausnutzt. Fast jede JavaScript-Anwendung war zu einem Zeitpunkt anfällig - insbesondere durch populäre Bibliotheken wie lodash (vor 4.17.12), jQuery (vor 3.4.0), und Dutzende weitere npm-Pakete. Die Schwachstelle ist subtil: normaler Code sieht harmlos aus, bis ein Angreifer __proto__ als Schlüssel verwendet.
JavaScript-Prototypen - Grundlage der Schwachstelle
Jedes JavaScript-Objekt hat einen internen [[Prototype]]:
const obj = {};
obj.__proto__ === Object.prototype // true
Object.prototype.isPrototypeOf(obj) // true
Eigenschaft-Lookup-Chain:
obj.toString()
// → obj selbst: kein toString → schaue in __proto__
// → Object.prototype: hat toString! → gefunden!
Das Problem - Mutation von Object.prototype:
const maliciousInput = JSON.parse('{"__proto__": {"polluted": true}}');
// Wenn dieser Input in eine merge-Funktion fließt:
function merge(target, source) {
for (let key in source) {
target[key] = source[key]; // ← GEFÄHRLICH ohne Schlüssel-Validierung
}
}
merge({}, maliciousInput);
// Jetzt ist JEDES neue Objekt vergiftet:
const anyObj = {};
console.log(anyObj.polluted); // true ← PROTOTYPE POLLUTION!
Drei Angriffspfade für __proto__-Manipulation:
// 1. Direkte __proto__-Zuweisung:
obj["__proto__"]["isAdmin"] = true;
// 2. constructor.prototype:
obj["constructor"]["prototype"]["isAdmin"] = true;
// 3. __proto__ als JSON-Key:
JSON.parse('{"__proto__": {"isAdmin": true}}')
// → Object.prototype.isAdmin = true → JEDES Objekt hat jetzt isAdmin!
Ausnutzbare Szenarien
1. Privilege Escalation (häufigster Impact)
// Anwendungs-Code (vereinfacht):
function checkAdmin(user) {
return user.isAdmin === true;
}
// Normale Anfrage:
const user = { name: "Alice" };
checkAdmin(user); // false (isAdmin nicht gesetzt)
// Nach Prototype Pollution:
// Angreifer hat {"__proto__": {"isAdmin": true}} übergeben
const user = { name: "Bob" }; // NEUES Objekt!
user.isAdmin // true ← via Prototype Chain!
checkAdmin(user); // true ← PRIVILEGE ESCALATION!
// Realistisches Beispiel - RBAC-Bypass:
if (!req.user.roles.includes('admin')) { return 403; }
// Nach Pollution: jeder User hat roles via Prototype-Chain!
2. Remote Code Execution in Node.js (via Template-Engines)
Wenn Prototype Pollution auf Server-Seite ausgenutzt wird, können Template-Engines wie EJS, Pug oder Handlebars betroffen sein: Diese nutzen Object.prototype-Properties intern. Die Pollution bestimmter Properties (outputFunctionName, escapeFunction) kann zu Code-Ausführung führen wenn ein Template gerendert wird. Bekannte Gadget-Chains sind für EJS, Pug und Handlebars dokumentiert (CVE-2021-23358, CVE-2022-24999 und ähnliche Advisories).
3. DoS via Objekt-Manipulation
Object.prototype.toString = null;
// JEDER JSON.stringify-Aufruf wirft TypeError!
// → Denial of Service der gesamten Anwendung
4. Prototype Pollution → XSS (Client-Side)
// jQuery < 3.4.0 (historisch):
$.extend(true, {}, JSON.parse(userInput));
// __proto__ Pollution → jQuery-interne Properties vergiftet
// → XSS möglich via Event-Handler-Injection
Betroffene npm-Pakete (historisch)
Bekannte anfällige Bibliotheken (bereits gepatcht!):
| Paket | Betroffene Funktion | 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, ...) mit __proto__ Input | CVE-2019-11358 | jQuery >= 3.4.0 |
| hoek < 5.0.3 (hapi.js) | hoek.merge() / hoek.clone() | CVE-2018-3728 | Update auf >= 5.0.3 |
| flat < 5.0.1 | flat.unflatten({'__proto__.polluted': true}) | - | Update auf >= 5.0.1 |
| minimist < 1.2.6 | CLI-Argument-Parser (verbreitet!) | CVE-2020-7598 | Update auf >= 1.2.6 |
# Erkennung mit npm audit:
npm audit --audit-level=high
# → Zeigt Prototype-Pollution-Schwachstellen in Dependencies
# Erkennung mit Snyk:
npx snyk test
# → Tiefere Analyse der Dependency-Kette
Erkennung im Pentest
Black-Box Test - JSON-Input mit __proto__
POST /api/settings HTTP/1.1
Content-Type: application/json
{"settings": {"__proto__": {"admin": true}}}
Alternative Syntax:
{"settings": {"constructor": {"prototype": {"admin": true}}}}
Tiefe Merge-Payload:
{"a": {"b": {"c": {"__proto__": {"polluted": "yes"}}}}}
Pollution-Nachweis: GET /api/profile oder GET /api/user - enthält Response neue Properties?
Parameter Pollution in Query-Strings
GET /api?__proto__[admin]=true
GET /api?constructor[prototype][admin]=true
Automatisiertes Scanning
- Burp Suite Intruder: alle JSON-Keys auf
__proto__-Varianten testen - PPMap / PPScan npm-Tools für automatische Black-Box-Erkennung
Code Review (White-Box)
# Unsichere Merge-Patterns ohne Schlüssel-Validierung:
grep -r "Object.assign\|\.merge\|\.extend" src/
grep -r "for.*in.*source" src/ # for..in iteriert __proto__!
Schutzmaßnahmen
1. Schlüssel-Validierung vor Objekt-Zuweisung
// Unsicher:
target[key] = source[key];
// Sicher - gefährliche Schlüssel ausschließen:
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
if (!FORBIDDEN_KEYS.has(key)) {
target[key] = source[key];
}
// Oder mit Object.hasOwn() (kein Prototype-Chain-Lookup):
if (Object.hasOwn(source, key)) {
target[key] = source[key];
}
2. Object.create(null) für Dictionaries ohne Prototype
// Normales Objekt: erbt von Object.prototype (anfällig)
const obj = {};
// Null-Prototype-Objekt: keine Prototype-Chain (sicher!)
const safeMap = Object.create(null);
// safeMap.__proto__ → undefined
// Pollution nicht möglich!
3. structuredClone() statt manueller Merge (Node.js 17+)
// Unsicher (wenn merge keine Schlüssel-Validierung hat):
const config = customMerge({}, userInput);
// Sicher (behandelt __proto__ korrekt):
const config = structuredClone(userInput);
4. JSON Schema Validation vor Verarbeitung
import Ajv from 'ajv';
const ajv = new Ajv();
const schema = {
type: 'object',
properties: { name: { type: 'string' } },
additionalProperties: false, // ← Blockt __proto__ und andere!
};
if (!ajv.validate(schema, input)) throw new Error('Invalid input');
5. Object.freeze(Object.prototype) (Notfall-Hardening)
// Verhindert Mutation von Object.prototype:
Object.freeze(Object.prototype);
// Alle Prototype-Pollution-Versuche silently fail oder TypeError!
Achtung: Kann Legacy-Code brechen → erst testen!
6. Dependencies aktuell halten
-
npm auditregelmäßig ausführen - Renovate/Dependabot für automatische Updates
- Snyk in CI/CD-Pipeline für Dependency-Scanning
-
package.jsonOverrides für transitive Dependencies mit PP-Schwachstellen