Prototype Pollution in JavaScript verstehen und verhindern
AI generated
OWASP
0x00
Prototype Pollution · JavaScript-Sicherheit
Prototype Pollution in JavaScript verhindern
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.

16 Min. Lesezeit Prototype Pollution JavaScript-Sicherheit

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.

11. FAQ: Prototype Pollution: Das Wichtigste auf einen Blick

1Betrifft Prototype Pollution nur Node.js oder auch den Browser?
Beide, überall dort, wo eine unsichere Merge-Funktion Objekte aus nutzergesteuerten Daten zusammenführt.
2Reicht JSON.parse allein als Schutz?
Nein, JSON.parse erzeugt zwar keinen echten __proto__-Zugriff im geparsten Objekt selbst, das Problem entsteht erst in der nachfolgenden Merge-Funktion.
3Ist Object.freeze(Object.prototype) ein vollständiger Schutz?
Nein, es ist ein zusätzliches Sicherheitsnetz, ersetzt aber keine sichere Merge-Implementierung.
4Warum ist Map sicherer als ein gewöhnliches Objekt?
Map besitzt keine Prototypenkette im gleichen Sinne, Schlüssel wie __proto__ werden dort als gewöhnliche, harmlose Zeichenketten behandelt.
5Kann lodash.merge Prototype Pollution verursachen?
Ältere Versionen waren betroffen und wurden gepatcht, aktuelle Versionen sollten verwendet und regelmäßig per npm audit geprüft werden.
6Wie erkenne ich, ob meine Anwendung verwundbar ist?
Alle rekursiven Merge-/Extend-/Clone-Funktionen im eigenen Code und in Abhängigkeiten auf fehlende Schlüsselprüfung untersuchen.
7Führt Prototype Pollution immer zu Remote Code Execution?
Nein, nur wenn eine nachgelagerte Komponente die manipulierte Eigenschaft in eine gefährliche Operation wie eval() übersetzt.
8Sollte ich constructor und prototype auch blockieren, nicht nur __proto__?
Ja, alle drei Schlüssel ermöglichen verwandte Angriffe und sollten konsequent gemeinsam blockiert werden.
9Hilft TypeScript gegen Prototype Pollution?
Nur begrenzt, da TypeScript-Typen zur Laufzeit nicht mehr existieren und die Schwachstelle ein Laufzeitproblem ist.
10Gibt es fertige, sichere Merge-Bibliotheken?
Ja, moderne Versionen etablierter Bibliotheken haben explizite Prototype-Pollution-Schutzmaßnahmen eingebaut, aktuelle Version und Changelog prüfen.