Wie eine unsichere Merge-Funktion Angreifern erlaubt, den globalen Object.prototype zu manipulieren und damit die gesamte Anwendung zu kompromittieren
Prototype Pollution ist eine JavaScript-spezifische Schwachstellenklasse, bei der ein Angreifer über eine unsichere Objektzusammenführung Eigenschaften auf `Object.prototype` selbst schreibt statt auf das eigentlich gemeinte Zielobjekt, wodurch diese manipulierten Eigenschaften anschließend auf jedem neu erstellten Objekt der gesamten Anwendung erscheinen, oft mit gravierenden Folgen bis hin zu Remote Code Execution. Diese Schwachstelle betrifft sowohl clientseitigen Browser-Code als auch serverseitigen Node.js-Code und ist besonders tückisch, weil der auslösende Merge-Code auf den ersten Blick völlig harmlos aussieht.
Inhaltsverzeichnis
- 1. Kurzer Exkurs: Wie JavaScript-Prototypen funktionieren
- 2. Der Angriffsvektor: __proto__-Injection über Merge-Funktionen
- 3. Ein verwundbares Merge-Beispiel und seine Auswirkung
- 4. Von Logikfehlern bis zu Remote Code Execution
- 5. Sichere Merge-Implementierung mit expliziter Schlüsselprüfung
- 6. Object.freeze(Object.prototype) als zusätzliche Verteidigungsebene
- 7. Map statt gewöhnlichem Objekt für nutzergesteuerte Schlüssel
- 8. Abhängigkeiten auf bekannte Prototype-Pollution-CVEs prüfen
- 9. Prototype Pollution gezielt testen und erkennen
- 10. Zusammenfassung
- 11. FAQ
1. Kurzer Exkurs: Wie JavaScript-Prototypen funktionieren
In JavaScript besitzt praktisch jedes Objekt eine interne Verbindung zu einem Prototyp-Objekt, von dem es Eigenschaften und Methoden erbt, wenn diese nicht direkt auf dem Objekt selbst definiert sind. Der Prototyp eines gewöhnlichen, mit `{}` oder `new Object()` erzeugten Objekts ist `Object.prototype`, und da praktisch jedes Objekt in einer JavaScript-Anwendung letztlich in dieser Prototypenkette landet, wirkt sich jede Änderung an `Object.prototype` selbst potenziell auf jedes existierende UND jedes zukünftig erstellte Objekt der gesamten Anwendung aus, unabhängig davon, in welchem Modul oder welcher Bibliothek dieses Objekt später verwendet wird.
Diese globale Reichweite macht `Object.prototype` zu einem außergewöhnlich attraktiven Angriffsziel: Gelingt es einem Angreifer, eine Eigenschaft auf `Object.prototype` zu schreiben, statt auf ein gewöhnliches, lokal begrenztes Objekt, betrifft diese Manipulation potenziell jeden Codepfad der Anwendung, der irgendwann ein einfaches Objekt erstellt und auf eine Eigenschaft mit demselben Namen zugreift, selbst wenn dieser Codepfad in einem völlig anderen, scheinbar unzusammenhängenden Teil der Anwendung liegt.
2. Der Angriffsvektor: __proto__-Injection über Merge-Funktionen
Der klassische Angriffsweg führt über eine rekursive Merge- oder Extend-Funktion, wie sie in unzähligen Utility-Bibliotheken und selbstgeschriebenem Code existiert, um zwei Objekte tief zusammenzuführen, etwa um Nutzer-Konfigurationsoptionen mit Standardwerten zu kombinieren. Verarbeitet diese Funktion die Eigenschaftsnamen des Quellobjekts ungefiltert, kann ein Angreifer ein JSON-Objekt mit dem Schlüssel `__proto__` einschleusen (etwa als Body eines API-Requests), das von der Merge-Funktion als gewöhnlicher Eigenschaftsname interpretiert wird, aber von der JavaScript-Engine als spezieller Zugriff auf den Prototyp des Zielobjekts behandelt wird.
Da `__proto__` in den meisten JavaScript-Engines ein spezieller Accessor ist, der direkt auf den internen Prototyp eines Objekts zugreift statt eine gewöhnliche Eigenschaft zu setzen, führt `merge(zielObjekt, JSON.parse(angreiferInput))` mit einem präparierten Input wie `{"__proto__": {"isAdmin": true}}` dazu, dass die Eigenschaft `isAdmin` nicht auf `zielObjekt`, sondern auf `Object.prototype` selbst landet, wovon anschließend jedes neu erstellte, einfache Objekt in der gesamten Anwendung betroffen ist.
3. Ein verwundbares Merge-Beispiel und seine Auswirkung
Das folgende Beispiel zeigt eine naive, rekursive Merge-Funktion, wie sie vor der breiten Bekanntheit dieser Schwachstellenklasse in vielen Projekten (und auch in älteren Versionen bekannter npm-Pakete wie lodash vor entsprechenden Patches) tatsächlich in Produktion lief, ohne dass der Eigenschaftsname jemals gegen `__proto__`, `constructor` oder `prototype` geprüft wurde.
// VERWUNDBAR: keine Pruefung gefaehrlicher Schluesselnamen
function merge(ziel, quelle) {
for (const key in quelle) {
if (typeof quelle[key] === 'object' && quelle[key] !== null) {
if (typeof ziel[key] !== 'object') ziel[key] = {};
merge(ziel[key], quelle[key]);
} else {
ziel[key] = quelle[key];
}
}
return ziel;
}
// Angreifer-Input als JSON-Body eines API-Requests:
// { "__proto__": { "istAdmin": true } }
merge({}, JSON.parse(angreiferInput));
// Ab jetzt hat JEDES neu erstellte, einfache Objekt
// die Eigenschaft istAdmin === true, anwendungsweit:
console.log({}.istAdmin); // true
4. Von Logikfehlern bis zu Remote Code Execution
Die unmittelbaren Folgen einer erfolgreichen Prototype Pollution reichen von scheinbar harmlosen Logikfehlern (etwa eine Autorisierungsprüfung, die versehentlich `istAdmin` auf jedem Objekt als wahr ansieht) bis zu deutlich gravierenderen Angriffen. Besonders kritisch wird es, wenn eine durch Prototype Pollution injizierte Eigenschaft von einer nachgelagerten Bibliothek als Konfigurationsoption interpretiert wird, die intern `eval()`, `child_process.exec()` oder eine vergleichbar gefährliche Funktion aufruft, wodurch aus einer scheinbar harmlosen Objektverschmutzung tatsächlich Remote Code Execution werden kann.
Mehrere real dokumentierte CVEs in bekannten npm-Paketen (unter anderem in Template-Engines und Konfigurationsbibliotheken) zeigten genau diese Eskalationskette: Prototype Pollution über eine ungeprüfte Merge-Funktion, gefolgt von der Ausnutzung einer nachgelagerten, eigentlich vertrauenswürdigen Bibliothek, die die manipulierte Prototyp-Eigenschaft ungeprüft in eine Codeausführung übersetzte.
5. Sichere Merge-Implementierung mit expliziter Schlüsselprüfung
Die direkteste Absicherung besteht darin, jede rekursive Merge- oder Extend-Funktion explizit gegen die drei gefährlichen Schlüsselnamen `__proto__`, `constructor` und `prototype` zu prüfen und diese Schlüssel konsequent zu überspringen, bevor überhaupt eine Zuweisung stattfindet, statt sich auf eine externe Bibliothek zu verlassen, deren Sicherheitsstand man nicht kennt.
// SICHER: gefaehrliche Schluessel werden explizit blockiert
const GEFAEHRLICHE_SCHLUESSEL = new Set(['__proto__', 'constructor', 'prototype']);
function sicherMerge(ziel, quelle) {
for (const key of Object.keys(quelle)) {
if (GEFAEHRLICHE_SCHLUESSEL.has(key)) continue;
if (typeof quelle[key] === 'object' && quelle[key] !== null) {
if (typeof ziel[key] !== 'object') ziel[key] = {};
sicherMerge(ziel[key], quelle[key]);
} else {
ziel[key] = quelle[key];
}
}
return ziel;
}
6. Object.freeze(Object.prototype) als zusätzliche Verteidigungsebene
Als zusätzliche, defensive Maßnahme lässt sich `Object.prototype` beim Anwendungsstart mit `Object.freeze(Object.prototype)` einfrieren, wodurch jeder spätere Versuch, eine neue Eigenschaft auf `Object.prototype` zu schreiben, im Strict Mode fehlschlägt (eine `TypeError`-Exception wirft) statt still zu funktionieren. Dieser Ansatz wirkt als Sicherheitsnetz, das eine Prototype-Pollution-Schwachstelle selbst dann noch abfängt, wenn sie an einer unerwarteten, noch nicht abgesicherten Stelle im Code existiert.
Diese Maßnahme ist allerdings kein Ersatz für eine sichere Merge-Implementierung, sondern eine zusätzliche Absicherung, da sie in einigen seltenen, legitimen Anwendungsfällen (etwa manche Polyfill-Bibliotheken, die absichtlich `Object.prototype` erweitern) zu Kompatibilitätsproblemen führen kann und deshalb sorgfältig gegen die eigene Bibliotheksabhängigkeiten getestet werden sollte, bevor sie in Produktion aktiviert wird.
7. Map statt gewöhnlichem Objekt für nutzergesteuerte Schlüssel
Wo immer Anwendungscode ein Objekt als Nachschlagetabelle mit nutzergesteuerten Schlüsseln verwendet, etwa um Konfigurationswerte anhand eines aus der Anfrage stammenden Namens zu speichern, ist ein natives `Map`-Objekt der strukturell sicherere Ersatz für ein gewöhnliches `{}`-Objekt, da `Map`-Instanzen keine Prototypenkette im gleichen Sinne besitzen und Schlüssel wie `__proto__` dort als vollkommen gewöhnliche, harmlose Zeichenketten behandelt werden, ohne jede Sonderbedeutung.
Dieser Architekturwechsel von Objekt zu `Map` löst das Problem an der Wurzel, statt es nur durch eine Schlüssel-Blockliste abzufangen, und ist deshalb die robusteste langfristige Lösung überall dort, wo neuer Code für nutzergesteuerte Nachschlagetabellen geschrieben wird.
8. Abhängigkeiten auf bekannte Prototype-Pollution-CVEs prüfen
Da viele Prototype-Pollution-Schwachstellen historisch in weit verbreiteten npm-Paketen selbst lagen, nicht nur in selbstgeschriebenem Code, gehört eine regelmäßige Prüfung der eigenen Abhängigkeiten mit `npm audit` oder einem vergleichbaren Werkzeug zur Grundabsicherung, da diese Werkzeuge bekannte CVEs, einschließlich zahlreicher dokumentierter Prototype-Pollution-Fälle, gegen die installierten Paketversionen abgleichen und veraltete, verwundbare Versionen aufzeigen.
Besonders relevant ist diese Prüfung bei Paketen, die Objekte aus externen, nutzergesteuerten Daten zusammenführen, parsen oder klonen, etwa Query-String-Parser, JSON-Schema-Validatoren oder Konfigurationslader, da genau diese Kategorie von Bibliotheken historisch am häufigsten von Prototype-Pollution-CVEs betroffen war.
9. Prototype Pollution gezielt testen und erkennen
Ein gezielter Test schickt bewusst präparierte Payloads mit den Schlüsseln `__proto__`, `constructor.prototype` und verwandten Varianten an jeden Endpunkt, der nutzergesteuerte JSON-Daten entgegennimmt, und prüft anschließend, ob `Object.prototype` tatsächlich verändert wurde, etwa durch Auslesen eines Testschlüssels auf einem frisch erzeugten leeren Objekt nach dem Request. Automatisierte Werkzeuge und spezialisierte Prototype-Pollution-Scanner in gängigen Security-Testing-Suiten übernehmen diese Prüfung systematisch über die gesamte API-Oberfläche hinweg, statt jeden Endpunkt einzeln von Hand durchzuprobieren.
Zusätzlich lohnt sich ein statischer Code-Scan, der rekursive Merge-, Extend- und Clone-Funktionen im eigenen Code automatisch identifiziert und markiert, damit jede dieser Funktionen gezielt gegen die in diesem Artikel beschriebenen Schutzmaßnahmen überprüft werden kann, statt auf einen zufälligen Fund während eines Penetrationstests angewiesen zu sein. Idealerweise wird diese Prüfung fest und dauerhaft in die CI-Pipeline integriert, sodass ein neu hinzugefügter, ungeschützter Merge-Aufruf schon vor dem eigentlichen Merge in den Hauptbranch zuverlässig auffällt, statt erst Monate später bei einem externen Sicherheitsaudit oder, schlimmer noch, erst nach einem tatsächlichen Sicherheitsvorfall in der echten Produktionsumgebung entdeckt zu werden.
| Maßnahme | Schutzwirkung | Aufwand |
|---|---|---|
| Schlüsselprüfung in Merge-Funktionen | Blockiert den Angriffsvektor direkt an der Quelle | Gering, einmalige Implementierung |
| Object.freeze(Object.prototype) | Sicherheitsnetz gegen unbekannte Lücken | Gering, Kompatibilität vorab prüfen |
| Map statt Objekt | Löst das Problem strukturell | Mittel, teils Refactoring nötig |
| npm audit / Dependency-Scanning | Deckt Schwachstellen in Abhängigkeiten auf | Gering, in CI automatisierbar |
Mironsoft
Security-Audits, OWASP-konforme Härtung und sichere Architektur
Anwendungen, die einem echten Angriffsversuch tatsächlich standhalten?
Wir prüfen bestehende Anwendungen auf klassische OWASP-Schwachstellen, unsichere Authentifizierung und fehlende Input-Validierung und bauen daraus eine Architektur, die Angriffsflächen strukturell reduziert statt nur einzelne Symptome zu flicken.
Security-Audit
OWASP Top 10, Auth-Flows und Input-Validierung systematisch auf Schwachstellen prüfen.
Sichere Architektur
Rate-Limiting, Verschlüsselung und Zugriffskontrollen von Grund auf richtig aufbauen.
Incident-Vorbereitung
Logging, Monitoring und Reaktionsprozesse für den Ernstfall etablieren.
10. Zusammenfassung
Prototype Pollution: Das Wichtigste auf einen Blick
Kernidee
Eine unsichere Merge-Funktion kann __proto__ als Schlüssel akzeptieren und dadurch Object.prototype selbst verändern.
Reichweite
Eine Änderung an Object.prototype betrifft jedes neu erstellte, einfache Objekt der gesamten Anwendung.
Beste Lösung
Gefährliche Schlüssel (__proto__, constructor, prototype) in jeder Merge-Funktion explizit blockieren.
Eskalation
Kann bis zu Remote Code Execution eskalieren, wenn eine nachgelagerte Bibliothek die manipulierte Eigenschaft in Codeausführung übersetzt.