Prototype Pollution
CWE-1321OWASP A03:2021Aktualisiert 31. August 20267 Min. Lesezeit
Prototype Pollution ist eine Schwachstelle, bei der ein Angreifer über Schlüssel wie __proto__ Eigenschaften an Object.prototype hängt, die danach fast jedes Objekt im Prozess erbt. Der Weg führt meist über eine rekursive Merge-Funktion und endet in einer umgangenen Berechtigungsprüfung oder in Codeausführung. Die Lösung: gefährliche Schlüssel abweisen und Eingaben gegen ein striktes Schema prüfen.
Viele JavaScript-Anwendungen führen Benutzereingaben mit einem Objekt zusammen, das sie bereits halten: eine geänderte Einstellung, eine ergänzte Konfiguration, ein teilweise überschriebenes Profil. Übernimmt eine rekursive Merge-Funktion dabei jeden übergebenen Schlüssel ungeprüft, kann ein Angreifer einen Schlüssel mitschicken, der gar nicht im Objekt landet, sondern in der gemeinsamen Vorlage dahinter. Das ist Prototype Pollution. Im Folgenden lesen Sie, wie das funktioniert und was Sie dagegen tun.
Was ist Prototype Pollution?
Prototype Pollution ist eine Schwachstelle, bei der ein Angreifer Eigenschaften in dem Prototyp ergänzt oder verändert, von dem JavaScript-Objekte erben, sodass anschließend fast jedes andere Objekt im Prozess diese Eigenschaften übernimmt. Jedes gewöhnliche Objekt erbt in JavaScript von Object.prototype. Fragt Code eine Eigenschaft ab, die ein Objekt selbst nicht besitzt, sucht die Engine in dieser Prototypenkette weiter und liefert zurück, was sie dort findet. Wer in den Prototyp schreibt, antwortet also stellvertretend für alle Objekte zugleich.
Ein Vergleich aus dem Alltag: Eine Druckerei bewahrt ein einziges Musterformular auf, und jeder Blankovordruck am Schalter ist eine Kopie davon. Ergänzt jemand auf dem Muster eine Zeile im Kleingedruckten, trägt jedes danach gedruckte Formular diese Zeile, obwohl niemand die bereits ausgegebenen Exemplare angefasst hat. Object.prototype ist dieses Musterformular.
Zwei Türen führen dorthin: Bei einem gewöhnlichen Objekt verweist __proto__ auf dessen Prototyp, und der Umweg über constructor gefolgt von prototype endet an derselben Stelle. Ein Filter, der nur auf den ersten Namen achtet, lässt den zweiten Weg weit offen.
Die Schwachstelle tritt in zwei Ausprägungen auf. Im Browser erreicht die Eingabe die Prototypenkette über den Query String, den Fragmentbezeichner oder eine Nachricht zwischen Fenstern, und der Angriff endet meist in DOM-basiertem Cross-Site Scripting. Serverseitig, üblicherweise in Node.js, gelangt sie über einen JSON-Body oder einen verschachtelt geschriebenen Query String hinein, mit einer umgangenen Berechtigungsprüfung oder Remote Code Execution (RCE) als Ergebnis, also beliebigem Code auf Ihrem Server.
Wie funktioniert ein Prototype-Pollution-Angriff?
Nehmen wir einen Endpunkt, über den ein Benutzer seine eigenen Einstellungen ändert: Die Anwendung lädt die gespeicherten Einstellungen, führt die übermittelten Felder hinein und schreibt das Ergebnis zurück. Die Merge-Funktion ist selbst geschrieben und läuft rekursiv durch verschachtelte Objekte.
Verwundbar:
// Selbst geschriebener Deep Merge: übernimmt jeden übergebenen Schlüssel
function merge(target, source) {
for (const key of Object.keys(source)) {
const value = source[key];
if (value !== null && typeof value === "object") {
// Das Lesen von target[key] liefert bei __proto__ den Prototyp selbst
if (typeof target[key] !== "object" || target[key] === null) {
target[key] = {};
}
merge(target[key], value);
} else {
target[key] = value;
}
}
return target;
}
app.post("/api/settings", express.json(), (req, res) => {
const settings = merge(loadSettings(req.user.id), req.body);
saveSettings(req.user.id, settings);
res.json(settings);
});
Eine gewöhnliche Anfrage enthält ein Farbschema und liefert die aktualisierten Einstellungen zurück. Ein Angreifer ergänzt den Body um einen einzigen Schlüssel:
POST /api/settings HTTP/1.1
Host: app.example.com
Content-Type: application/json
{"theme":"dark","__proto__":{"isAdmin":true}}
Zwei Eigenheiten der Sprache machen das möglich. Erstens behandelt JSON.parse den Schlüssel __proto__ als gewöhnliche eigene Eigenschaft des erzeugten Objekts, der Schlüssel überlebt das Einlesen also und erreicht die Schleife. Zweitens verhält sich das Lesen von target["__proto__"] gerade nicht gewöhnlich: Es liefert keine Eigenschaft des Zielobjekts, sondern Object.prototype selbst. Die Rekursion schreibt isAdmin anschließend direkt in den Prototyp.
Ab diesem Moment ist der gesamte Prozess verunreinigt, für jeden Benutzer, den er bedient:
const account = {};
account.isAdmin; // true, ohne jede Zuweisung
Object.hasOwn(account, "isAdmin"); // false
Jede Prüfung, die lediglich fragt, ob der Wert wahr ist, lässt nun alle durch: Eine Middleware, die den Administrationsbereich bei gesetzter Markierung öffnet, sieht sie überall gesetzt. Derselbe Zug greift bei Optionen, die eine Bibliothek ausliest und deren Fehlen sie stillschweigend voraussetzt. Eine solche Option nennt man ein Gadget. Verunreinigen Sie die Option, die entscheidet, ob ein Unterprozess über eine Shell gestartet wird, oder ein Feld, das eine Template-Engine als Code auswertet, und aus der Verunreinigung wird Codeausführung.
Die Reparatur hat zwei Hälften: die gefährlichen Schlüssel abweisen und niemals in ein Objekt absteigen, dessen Prototyp Sie nicht selbst gewählt haben.
Sicher:
const VERBOTEN = new Set(["__proto__", "constructor", "prototype"]);
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (VERBOTEN.has(key)) continue;
const value = source[key];
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
const vorhanden = Object.hasOwn(target, key) ? target[key] : null;
if (vorhanden === null || typeof vorhanden !== "object") {
// Ohne Prototyp: "__proto__" ist hier ein gewöhnlicher Schlüssel
target[key] = Object.create(null);
}
safeMerge(target[key], value);
} else {
target[key] = value;
}
}
return target;
}
Noch besser ist es, formfreie Eingaben gar nicht zusammenzuführen. Beschreiben Sie die erwartete Gestalt in einem Schema und verwerfen Sie unbekannte Felder, dann erreicht __proto__ die Merge-Funktion nicht:
import { z } from "zod";
const Settings = z.object({
theme: z.enum(["light", "dark"]),
notifications: z.object({ email: z.boolean() }).strict()
}).strict();
app.post("/api/settings", (req, res) => {
const parsed = Settings.safeParse(req.body);
if (!parsed.success) {
return res.status(400).send("Ungültige Einstellungen");
}
const settings = safeMerge(loadSettings(req.user.id), parsed.data);
saveSettings(req.user.id, settings);
res.json(settings);
});
Bestimmt der Benutzer die Schlüssel selbst, halten Sie sie aus gewöhnlichen Objekten heraus: Eine Map und ein Objekt ohne Prototyp besitzen keine Prototypenkette.
const versuche = new Map();
versuche.set(req.ip, anzahl); // Schlüssel bleiben Daten, nie Eigenschaften
const labels = Object.create(null);
labels[req.query.name] = value; // "__proto__" ist hier ein normaler Schlüssel
__proto__ enthält, genügt nicht, denn über constructor und danach prototype erreicht ein Angreifer dasselbe Objekt. Auch ein Filter, der die Zeichenfolge einmalig aus einem Schlüssel entfernt, lässt sich umgehen, weil dabei erneut ein gültiger Schlüssel entstehen kann. Weisen Sie alle drei Schlüssel vollständig ab, statt sie zu bereinigen.Welche Auswirkungen hat Prototype Pollution?
Prototype Pollution ist selten für sich genommen das Ende und selten harmlos. Der Schweregrad hängt vom Gadget ab: Die verunreinigte Eigenschaft bewirkt erst etwas, wenn anderer Code sie ausliest und eine Entscheidung darauf stützt. Fehlt es, bleiben merkwürdiges Verhalten oder ein abgestürzter Prozess, bereits ein Denial of Service, denn die Verunreinigung gilt für den gesamten Prozess und verschwindet erst mit einem Neustart.
Gibt es ein Gadget, eskaliert die Lage rasch. Eine verunreinigte Eigenschaft in einer Berechtigungsprüfung ergibt eine umgangene Autorisierung, mit der ein Angreifer administrative Funktionen erreicht, ohne je ein Passwort zu erraten. Ein Gadget in einer Template-Engine oder in den Optionen eines Aufrufs an ein externes Programm ergibt Codeausführung auf dem Server: Schlüssel lesen, Daten abfließen lassen, auf interne Systeme vorstoßen. Im Browser endet derselbe Fehler in Cross-Site Scripting und damit in übernommenen Sitzungen.
Geschäftlich wiegt eine Eigenheit besonders schwer: Der Schaden bleibt nicht auf die Sitzung des Angreifers beschränkt. Weil alle Objekte im Prozess dieselbe Vorlage teilen, kann eine einzige Anfrage die Verarbeitung gleichzeitig aktiver Benutzer verändern, was in einer von mehreren Kunden geteilten Umgebung besonders unangenehm ist. Die Einstufung reicht deshalb von mittel bis hoch.
Wie erkennen Sie Prototype Pollution?
Beginnen Sie dort, wo Benutzereingaben ohne feste Gestalt in ein Objekt gelangen: bei Funktionen mit Namen wie merge, extend, clone oder set, bei Parsern für Query Strings mit verschachtelter Schreibweise, bei Konfigurationsladern und bei Endpunkten, die einen JSON-Body unmittelbar über ein gespeichertes Objekt legen. In fremdem Code ist das ein Klassiker, deshalb gehört eine Analyse Ihrer Abhängigkeiten in dieselbe Runde.
Getestet wird, indem Sie eine Payload mit __proto__ im JSON-Body senden, denselben Versuch über den Query String als ?__proto__[verunreinigt]=ja wiederholen, zusätzlich die Variante mit constructor und prototype verwenden und jeweils prüfen, ob ein frisch erzeugtes Objekt die Eigenschaft geerbt hat. Bleibt eine Rückmeldung aus, liegt ein blinder Fall vor, und Sie brauchen einen sichtbaren Nebeneffekt: Verunreinigen Sie eine Einstellung, die das Framework ausliest, etwa die Option, mit der Express die Einrückung von JSON-Antworten festlegt, und achten Sie auf die Formatierung der folgenden Antworten. Auf der Clientseite setzen Sie über die URL eine Eigenschaft und prüfen in der Konsole, ob ein leeres Objekt sie erbt.
Scanner melden bekannte verwundbare Abhängigkeiten, erkennen aber eine selbst geschriebene Merge-Funktion kaum, geschweige denn das Gadget, das aus der Verunreinigung einen Exploit macht. Das erfordert das Lesen von Code und manuelles Weiterdenken von der Verunreinigung bis zur Wirkung. AssistSec prüft diesen Bereich im Rahmen eines Penetrationstests und weist je Befund nach, welcher Schlüssel den Prototyp erreicht hat und welches Gadget daraus etwas gemacht hat.
Wie verhindern Sie Prototype Pollution?
- Weisen Sie die Schlüssel
__proto__,constructorundprototypeam Rand ab. Tun Sie das beim Einlesen von allem, was von außen kommt, nicht erst tief in der Verarbeitung, wo ein Codepfad die Prüfung überspringen kann. - Prüfen Sie gegen ein striktes Schema. Beschreiben Sie die erwarteten Felder mit einem Validator wie zod, Joi oder Ajv und verwerfen Sie unbekannte Felder ausdrücklich. Dieselbe Maßnahme schließt Mass Assignment aus.
- Nutzen Sie
Object.create(null)oder eineMapfür benutzergesteuerte Schlüssel. Beides besitzt keine Prototypenkette, ein nach dem Prototyp benannter Schlüssel wird also als Datum gespeichert. - Schreiben Sie keinen eigenen Deep Merge für unvertrauenswürdige Eingaben. Verwenden Sie eine gepflegte Bibliothek, prüfen Sie eigene Eigenschaften mit
Object.hasOwnund steigen Sie nie in ein Objekt ab, das Sie nicht selbst angelegt haben. - Sichern Sie die Laufzeitumgebung ab, wo es geht. Node.js kennt die Option
--disable-protomit den Werten throw und delete. Das Einfrieren vonObject.prototypehilft ebenfalls, testen Sie es aber zuerst: Manche Bibliotheken schreiben selbst in Prototypen. - Halten Sie Abhängigkeiten aktuell und lassen Sie den Code prüfen. Merge-, Query-String- und Konfigurationsbibliotheken sind die bekannteste Quelle; ein Review jeder Zusammenführungsroutine und ein gezielter Penetrationstest fangen ab, was ein Scanner überspringt.
Quellen
Häufige Fragen
Was ist der Unterschied zwischen Prototype Pollution und Mass Assignment?
Bei Mass Assignment beschreibt ein Angreifer ein Feld, das tatsächlich existiert, aber nicht änderbar sein dürfte, etwa ein Rollenfeld im eigenen Konto. Bei Prototype Pollution schreibt er am Datensatz vorbei in die gemeinsame Vorlage, von der alle Objekte erben. Die Folgen ähneln sich, die Reichweite nicht: Die verunreinigte Eigenschaft erscheint in jedem Objekt, das sie nicht selbst setzt, auch in Objekten anderer Benutzer.
Betrifft Prototype Pollution nur Node.js?
Nein. Die Schwachstelle steckt in der Sprache und nicht in der Laufzeitumgebung, deshalb ist Code im Browser genauso betroffen. Auf der Clientseite gelangt die Eingabe meist über den Query String, den Fragmentbezeichner der URL oder eine Nachricht zwischen Fenstern in die Prototypenkette, und der Angriff endet in DOM-basiertem Cross-Site Scripting. Serverseitig in Node.js wiegen die Folgen schwerer, weil dort mehr Gadgets in Richtung Codeausführung erreichbar sind.
Reicht es, __proto__ zu blockieren?
Nein. Derselbe Prototyp ist über den Schlüssel constructor gefolgt von prototype erreichbar, ein Filter gegen nur einen Namen lässt den anderen Weg offen. Filter, die die Zeichenfolge einmalig aus einem Schlüssel entfernen, lassen sich ebenfalls umgehen, weil dabei erneut ein gültiger Schlüssel entstehen kann. Weisen Sie alle drei Schlüssel vollständig ab oder nutzen Sie ein striktes Schema, das unbekannte Felder verwirft.
Wie schwerwiegend ist Prototype Pollution in der Praxis?
Das hängt vom Gadget ab, denn eine verunreinigte Eigenschaft bewirkt erst dann etwas, wenn anderer Code sie ausliest und eine Entscheidung darauf stützt. Ohne brauchbares Gadget bleiben merkwürdiges Verhalten oder ein abgestürzter Prozess, was für sich genommen bereits ein Denial of Service ist. Mit einem Gadget in einer Berechtigungsprüfung, einer Template-Engine oder den Optionen eines Aufrufs an ein externes Programm reicht die Wirkung bis zur vollständigen Übernahme, weshalb die Einstufung von mittel bis hoch reicht.
Verwandte Artikel
- SchwachstellenCWE-284A01:2021Broken Access ControlBroken Access Control erklärt: horizontale und vertikale Rechteausweitung, Forced Browsing und warum Deny by Default serverseitig gehört.
- SchwachstellenCWE-915A01:2021Mass AssignmentWer einen ganzen Anfragekörper auf ein Modell bindet, macht isAdmin oder guthaben beschreibbar. Lesen Sie, wie eine Positivliste das verhindert.
- SchwachstellenCWE-1104A06:2021Veraltete und verwundbare KomponentenEine veraltete Bibliothek oder ein alter Webserver trägt öffentlich bekannte Schwachstellen mit fertigem Angriffscode. Lesen Sie, wie Sie das beherrschen.
- SchwachstellenCWE-94A03:2021Remote Code Execution (RCE)Remote Code Execution (RCE) erklärt: wie Angreifer über ungefilterte Eingaben eigene Befehle oder Code auf Ihrem Server ausführen und wie Sie es verhindern.