Prototype Pollution ist eine eigentümlich JavaScript-spezifische Schwachstelle, die in den letzten Jahren zur eigenständigen Klasse geworden ist. Anders als XSS oder SQLi zielt sie nicht auf einzelne Eingaben, sondern auf das Prototyp-Modell von JavaScript selbst: wenn ein:e Angreifer:in Properties auf Object.prototype setzt, erbt jedes Objekt im Programm diese Properties — und Anwendungs-Logik, die mit Default-Werten rechnet, läuft anders als gedacht. Dieser Artikel erklärt das Konzept, die typischen Sinks, die historische CVE-Welle und die Schutz-Patterns.

Was Prototype Pollution ist

In JavaScript hat jedes Objekt eine Prototyp-Kette. Wenn du auf ein Property zugreifst, das ein Objekt selbst nicht hat, geht JavaScript die Prototyp-Kette hoch — bis zum Object.prototype als Wurzel. Wenn dort eine Property liegt, wird sie als „virtuelles Property" des Objekts wahrgenommen.

Konkret:

JavaScript prototype-grundlagen.js
const obj = {};
console.log(obj.foo); // undefined

// Wenn jemand Object.prototype manipuliert ...
Object.prototype.foo = 'gepwnt';

console.log(obj.foo); // 'gepwnt' — ohne dass obj selbst je foo hatte
console.log({}.foo);  // 'gepwnt' — gilt für ALLE Objekte

Wer Object.prototype modifiziert, verändert jedes Objekt im Programm. Wenn ein:e Angreifer:in Eingaben so platziert, dass sie zu einem Object.prototype-Property werden, ist die Wirkung global.

Wie der Angreifer dahinkommt:

JavaScript hat zwei Wege, an die Prototyp-Kette zu kommen:

  • __proto__ — älterer Property-Zugriff. In JSON-Eingaben oft durchschleifbar.
  • constructor.prototype — über den Constructor.
  • __proto__ über Object.assign / Spread — manche Implementierungen ignorieren __proto__ korrekt, andere nicht.

Typische Angriffs-Eingaben:

JSON pollution-payloads.json
{ "__proto__": { "isAdmin": true } }

{ "constructor": { "prototype": { "isAdmin": true } } }

Wenn diese Eingabe in eine rekursive Merge-Operation läuft, die Properties tief kopiert, landet isAdmin: true als globales Property auf Object.prototype. Anschließend hat jedes Objekt im Programm obj.isAdmin === true.

Klassische Sinks

Wo entstehen Prototype-Pollution-Lücken? Drei typische Code-Patterns:

1. Tiefe Merge-Operationen.

JavaScript merge-sink.js
// Schadhaft — rekursives Merge ohne __proto__-Schutz
function deepMerge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      target[key] = target[key] || {};
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

const config = {};
deepMerge(config, JSON.parse(userInput));
// Wenn userInput { "__proto__": { "isAdmin": true } } enthält,
// setzt deepMerge Object.prototype.isAdmin = true

2. Property-Setter über String-Pfade.

JavaScript path-set-sink.js
// Schadhaft — Set-by-path ohne __proto__-Schutz
function setByPath(obj, path, value) {
  const keys = path.split('.');
  let target = obj;
  for (let i = 0; i < keys.length - 1; i++) {
    if (!target[keys[i]]) target[keys[i]] = {};
    target = target[keys[i]];
  }
  target[keys.at(-1)] = value;
}

setByPath(config, '__proto__.isAdmin', true);
// setzt Object.prototype.isAdmin = true

3. Query-String / Form-Parser.

JavaScript qs-parser-sink.js
// Manche ältere qs-Versionen waren anfällig:
// ?__proto__[isAdmin]=true wurde zu { __proto__: { isAdmin: true } }
// ... und mit Object.assign in den Heap geschleust

4. Object-Cloning per JSON-Loop. Falsch implementierte deep-clone-Funktionen können __proto__ durchschleifen.

5. ORM- und Form-Library-Defaults. Manche Libraries setzen unsichere Default-Properties über Prototyp-Patterns, was kompromittierbar wird.

Was die Schwachstelle ausnutzt

Prototype Pollution allein ist meist nicht der Endpunkt — sie ist ein Eskalations-Vektor. Die Frage ist: was macht die Anwendung mit den manipulierten Default-Werten?

Typische Eskalationen:

  • Authentifizierungs-Bypass. Wenn ein Auth-Check liest if (user.isAdmin), und isAdmin ist nun durch Pollution true auf jedem Objekt — Bypass.
  • CSP-Bypass / XSS. Wenn die App eine Library nutzt, die ein Property aus Object.prototype liest (z. B. ein DOM-Library, ein Template-Engine), kann ein manipuliertes Property zu XSS führen.
  • RCE in Node.js. In bestimmten Konstellationen — z. B. wenn child_process.exec mit einem options-Objekt aufgerufen wird, das von Prototype Pollution betroffen ist — kann ein manipuliertes Default-Property zu RCE führen. Forschung dazu von Snyk und PortSwigger seit ca. 2018.
  • DoS. Manipulation von Default-Werten in Critical-Path-Code kann Anwendung zum Absturz bringen.

Reale Beispiele:

  • Lodash (CVE-2018-3721, CVE-2019-10744, CVE-2020-8203) — mehrere Wellen, jeweils Patches und neue Funde. _.merge, _.set, _.defaultsDeep waren betroffen.
  • Hoek (Hapi, CVE-2018-3728) — Pollution-Vektor in merge-Funktion.
  • minimist (CVE-2020-7598) — Command-Line-Parser-Library, häufig in Build-Tools.
  • jQuery 3.x (CVE-2019-11358) — Prototype Pollution in $.extend(true, ...).
  • qs (Query-String-Parser) — mehrere Versionen anfällig.
  • express-fileupload, mongoose, async, mqtt, set-value, dot-prop — Liste ließe sich verlängern.

Die Liste betroffener NPM-Pakete ist groß. Wer eine moderne Node.js-Anwendung baut, hat sehr wahrscheinlich indirekt Code im Dependency-Baum, der historisch Pollution-Bugs hatte.

Pollution verhindern

Mehrere komplementäre Schutz-Patterns:

1. Statt {} lieber Object.create(null) verwenden.

Objekte ohne Prototyp existieren — und sind immun gegen Pollution.

JavaScript object-create-null.js
// Pollution-resistent
const config = Object.create(null);
// config hat keine __proto__-Property; Setzen läuft als normales Property
config.__proto__ = { isAdmin: true };  // setzt nur lokal, kein Effekt auf andere
console.log({}.isAdmin); // undefined — globale Object.prototype unverändert

2. Object.freeze(Object.prototype).

Friert die Prototyp-Kette ein, sodass keine neuen Properties hinzufügbar sind.

JavaScript freeze-prototype.js
// Beim Programm-Start
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
Object.freeze(Function.prototype);

// Versuche, Properties zu setzen, scheitern silent oder werfen Error (strict mode)

Vorsicht: Manche Libraries verlassen sich auf Erweiterung von Prototypen — der Freeze kann Existing-Code brechen. Im Default-Setup eines neuen Projekts sicher; bei Migration zu testen.

3. Schema-Validierung mit Allowlist.

Statt rohe Objekte aus JSON-Eingaben durchzuschleifen, mit Schema-Library (Zod, Joi, Yup, Ajv) validieren. Erlaubte Properties explizit, andere verworfen. Verhindert sowohl Pollution als auch viele andere Klassen.

JavaScript zod-validation.js
import { z } from 'zod';

const ConfigSchema = z.object({
  name: z.string(),
  theme: z.enum(['light', 'dark']),
  maxItems: z.number().int().positive(),
}).strict();  // unbekannte Properties (inkl. __proto__) werden abgelehnt

const parsed = ConfigSchema.parse(JSON.parse(userInput));
// parsed ist garantiert frei von __proto__-Tricks

4. Sichere Merge-/Set-Libraries nutzen.

Aktualisierte Versionen von Lodash, Hoek u. a. haben Schutz eingebaut. Aber: nicht alle Funktionen sind safe — die Doku der Library lesen.

5. __proto__ und constructor als Schlüssel ablehnen.

Eigene Merge-/Set-Implementierungen können explizit gefährliche Keys filtern:

JavaScript safe-merge.js
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function safeMerge(target, source) {
  for (const key in source) {
    if (FORBIDDEN_KEYS.has(key)) continue;
    if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = target[key] || {};
      safeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

6. Map statt Plain-Objects für User-Input.

JavaScript-Maps haben kein Prototyp-Konzept im selben Sinn — sie sind strukturell anders. Wenn du User-Input als Lookup-Struktur brauchst, kann Map sicherer sein als {}.

Browser-Seite: Client-Side Prototype Pollution

Prototype Pollution gibt es auch im Browser, nicht nur in Node.js. Im Browser ist die typische Eskalation XSS statt RCE.

Beispiel-Sink im Frontend:

JavaScript csp-bypass-via-pollution.js
// Schadhaft — Library liest ein Default-Property
function safeRender(element, options) {
  const opts = Object.assign({}, options);
  if (opts.sanitize !== false) {
    element.innerHTML = sanitize(element.innerHTML);
  } else {
    element.innerHTML = element.dataset.raw;  // unsanitized
  }
}

// Wenn Object.prototype.sanitize = false gesetzt wurde,
// wird die "innerHTML = raw"-Variante immer genommen — XSS möglich

PortSwigger Research hat 2019/2020 systematisch Bibliotheken gescannt und Tausende Pollution-zu-XSS-Vektoren gefunden — viele in Mainstream-Libraries wie jQuery, Vue.js (in alten Versionen), Bootstrap-Komponenten.

Schutz im Frontend:

  • Build-Time-Audit der Dependencies (npm audit, Snyk, Dependabot).
  • Trusted Types in der CSP — verhindert, dass manipulierte Properties zu DOM-XSS werden. Siehe trusted-types (Kap 11).
  • Object.freeze(Object.prototype) auch im Browser-Code.

Erkennung in der Praxis

Prototype Pollution ist mit Standard-Werkzeugen schwer automatisch zu finden. Drei nützliche Ansätze:

1. Static Analysis. Tools wie Snyk Code, Semgrep, CodeQL haben Regeln für typische Pollution-Sinks. Sehr effektiv für eigene Anwendung — weniger für Dependencies.

2. Dynamic Testing. Burp Suite Pro mit dem DOM Invader-Plugin testet automatisch Client-Side-Pollution. Für Backend gibt es separate Module.

3. Manuelle Code-Reviews. Spezifisch auf:

  • merge, mergeDeep, extend, assign mit User-Input.
  • set-Funktionen, die String-Pfade akzeptieren.
  • Query-String-Parser ohne Schema.
  • Cookie-Parser.

4. Dependency-Updates. Die meisten Pollution-CVEs werden in den Hauptbibliotheken gefixt. Wer aktuell hält (Dependabot, Renovate), schließt sie automatisch.

Warum es so eine eigene Klasse ist

Prototype Pollution ist eigentümlich, weil es auf Sprach-spezifischen Eigenheiten beruht. In Python, Java, Go, Rust gibt es das Problem in dieser Form nicht — weil dort entweder kein Prototype-Modell existiert oder die Eingaben nicht so leicht auf interne Klassen-Strukturen mappen.

JavaScript-spezifisch ist daran:

  • Das Prototype-Modell als Sprach-Default.
  • Die dynamische Property-Assignment (obj['key'] = value für beliebige Strings).
  • Die JSON-Eingabe, die nahtlos in Objekte konvertiert wird, ohne Type-Information.

Die Klasse hat sich erst ab 2018 als eigenständig herauskristallisiert — vorher wurden Pollution-Bugs als „normale Logik-Fehler" gemeldet. Heute ist sie eine bekannte CVE-Kategorie und in OWASP-Cheat-Sheets dokumentiert.

Besonderheiten

Olivier Arteau hat das Phänomen 2018 populär gemacht

Der NorthSec-Vortrag „Prototype pollution attacks in NodeJS applications" (2018) von Olivier Arteau hat das Konzept ins Sicherheits-Mainstream gebracht. Vorher gab es einzelne CVEs, aber keine zusammenhängende Klasse.

PortSwigger Research zu Client-Side Pollution

Gareth Heyes hat 2020 systematisch Browser-Libraries auf Pollution-zu-XSS-Vektoren gescannt. Veröffentlichung 2022 dokumentiert systematische Test-Methodik. Das hat Client-Side-Pollution erstmals als eigenständiges Thema sichtbar gemacht.

Lodash hat viele Wellen mitgemacht

Lodash ist eine der meistgenutzten JS-Libraries (Hunderte Millionen Downloads pro Woche). Mehrere CVE-Wellen über die Jahre — jeder Patch behebt eine Methode, dann wird die nächste gefunden. Heute (Lodash 4.17.21+) sind die bekannten Vektoren gepatcht.

Object.freeze(Object.prototype) als "Strict Web Apps"

Manche Hochsicherheits-Stacks frieren Prototypes beim Programm-Start ein. Funktioniert in modernen Stacks (Next.js, Vite-Apps) meist ohne Probleme. Vorsicht bei älteren Libraries, die Prototyp-Erweiterungen erwarten.

Node.js 20+ hat ein Permission-Modell

Mit Node.js 20 (2023) kam ein experimentelles --permission-Flag, das Datei-System- und Netzwerk-Zugriffe einschränken kann. Reduziert die Eskalations-Möglichkeiten von Pollution-zu-RCE — die Schwachstelle bleibt aber theoretisch ausnutzbar.

TypeScript hilft nicht direkt

TypeScript-Compilation kennt Prototypes nicht als spezielle Klasse. Strict-Mode-Typings (noImplicitAny, strict) helfen indirekt, weil sie unsaubere Code-Patterns aufdecken — aber Pollution selbst ist eine Runtime-Klasse, die TS nicht direkt sieht.

Cross-Stack-Variante: Server-Side-Pollution via Frontend

Ein subtiler Vektor: Frontend baut ein Objekt mit User-Eingabe, schickt es als JSON an Backend. Backend nutzt Pollution-anfällige Library zum Verarbeiten. Frontend ist nicht direkt verwundbar, Backend schon — und der Vektor wird über die normale API-Strecke geliefert. Wichtig bei Architekturen mit mehreren Schichten.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu OWASP Top 10

Zur Übersicht