blueredix logo
info vuln-class-prototype-pollution

Prototype Pollution, verständlich erklärt

Eine JavaScript-spezifische Bug-Klasse, bei der Eingaben des Angreifers Eigenschaften am globalen Object-Prototype überschreiben und damit jedes Objekt der Anwendung infizieren. Vom kaputten Merge zum Auth-Bypass und zur Remote Code Execution.

Worum es geht

Jedes Objekt in JavaScript erbt Eigenschaften von einem versteckten Eltern-Objekt, dem Prototype. Einfache Objekte ({}) erben von Object.prototype, der Wurzel der Kette. Prototype Pollution entsteht, wenn ein Angreifer eine Eigenschaft auf diese Wurzel schreiben kann. Jedes Objekt, das die Anwendung jetzt oder in Zukunft erzeugt, erbt damit die vom Angreifer gesetzte Eigenschaft.

Das Setup ist fast immer dasselbe. Die Anwendung nimmt strukturierte Eingaben (JSON-Body, Querystring, Formulardaten) und mergt sie über einen rekursiven Helfer in ein bestehendes Objekt. Die Merge-Funktion filtert keine speziellen Schlüssel, also setzt eine Payload wie

{ "__proto__": { "isAdmin": true } }

isAdmin: true direkt auf Object.prototype. Zwei Sekunden später prüft ein völlig unabhängiger Codepfad an anderer Stelle “ist diese Person Admin?” und bekommt true zurück, weil die Eigenschaft jetzt auf jedem Objekt sichtbar ist.

Wo das herkommt

Das Muster lebt in einer Handvoll weit verbreiteter Node.js- Bibliotheken und Programmierstile:

  • Rekursive Deep-Merge-, Extend- und Set-by-Path-Hilfsfunktionen. lodash.merge, lodash.set, lodash.defaultsDeep, object-path, set-value, mixin-deep, dot-prop. Jedes davon hat eine veröffentlichte CVE: lodash CVE-2018-3721, CVE-2019-10744; minimist CVE-2020-7598; set-value CVE-2019-10747; und seit Jahren weitere.
  • Großzügige Querystring-Parser. qs (der Parser, den Express jahrelang als Standard nutzte) interpretiert ?__proto__[admin]=1 so, dass admin auf Object.prototype gesetzt wird. CVE-2022-24999 ist die Express-Variante, mit der das schließlich gepatcht wurde.
  • Selbst geschriebene “Optionen mergen”-Helfer. Das Muster ist so naheliegend, dass es laufend neu erfunden wird: über die Eingabe iterieren, Schlüssel auf das Zielobjekt kopieren, in verschachtelte Objekte rekursieren. In neun von zehn Fällen vergisst die Implementierung, __proto__ und constructor herauszufiltern.
  • Clientseitig, über die URL. “DOM-based Prototype Pollution”: die Seite liest location.hash oder einen Query-Parameter, parst ihn als JSON und mergt ihn in ein Konfigurationsobjekt. Eine serverseitige Lücke ist nicht einmal nötig.

Der Bug ist nicht spezifisch für lodash. Er ist eine Eigenschaft von JavaScript selbst in Verbindung mit Code, der nicht damit rechnet, dass die Schlüssel vom Angreifer beeinflusst sein könnten.

Warum das wichtig ist

Prototype Pollution setzt zunächst nur eine Eigenschaft auf Object.prototype. Ob daraus ein vollständiger Vorfall wird, hängt davon ab, was der übrige Code mit dieser Eigenschaft anfängt. Die typischen Eskalationspfade:

  • Umgehung von Authentifizierung oder Autorisierung. Code, der if (user.isAdmin) oder if (req.body.role === 'admin') prüft, liest jetzt das polluted Object.prototype.isAdmin von jedem Objekt, das es nicht überschreibt. Mehrere CVEs in CMS- Frameworks und SaaS-Adminoberflächen folgen genau diesem Muster.
  • Remote Code Execution. Nodes child_process.spawn nutzt ein Options-Objekt, dessen shell- und env-Eigenschaften polluted werden können. Ein polluted Object.prototype.shell = '/bin/sh -c angriffsbefehl' macht aus dem nächsten harmlosen spawn()-Aufruf an anderer Stelle der Anwendung eine RCE. Derselbe Trick greift bei Template-Engines (Handlebars CVE-2021-23383), bei Body-Parsern und bei Paket-Install-Hooks.
  • Cross-Site Scripting. Clientseitige Prototype Pollution trifft oft die Konfiguration eines HTML-Sanitisers oder einer Template- Hilfsfunktion. Beim nächsten Request rendert die Seite vom Angreifer kontrolliertes HTML, weil die polluted Eigenschaft die Sanitierung unbemerkt deaktiviert.
  • Crash und Verfügbarkeit. Eine polluted Eigenschaft kann Invarianten brechen, auf die der nachgelagerte Code angewiesen ist (etwa Object.prototype.toString = "..."). Die Anwendung wirft dann bei jedem Request Exceptions.

Wie Angreifer das tatsächlich ausnutzen

Die Ausnutzung läuft in drei Phasen:

  1. Sink finden. Ein Codepfad, der vom Angreifer beeinflussbare Eingaben durchläuft und in ein Objekt schreibt: ein JSON-Merge- Endpoint, ein Einstellungs-Import, ein Profil-Speichern.
  2. Pollute. Eine Payload mit Schlüsseln __proto__, constructor oder prototype schicken, die die Merge-Funktion bis zu Object.prototype hochwandern lassen.
  3. Gadget auslösen. Einen nachgelagerten Codepfad finden, der die soeben polluted Eigenschaft liest und ihrem Wert oder ihrer Wahrheit Glauben schenkt.

Schritte 1 und 3 sind die eigentliche Arbeit; Schritt 2 ist über alle Ziele hinweg dieselbe Payload. Öffentliche Exploit-Datenbanken katalogisieren pro populärem Framework dutzende Gadgets der Form “polluted Eigenschaft X → Eskalation Y”.

Wie das behoben wird

Drei Schichten, kombiniert angewendet:

Die richtige Datenstruktur für untrusted Input wählen.

  • Für Dictionary-artige Daten Map statt Plain Objects nutzen. Map läuft nicht über die Prototype-Kette, daher infiziert eine Pollution einer Map keine andere.
  • Für Objektliterale, die strukturell sicher sein sollen, Object.create(null) verwenden. Das Objekt hat keinen Prototype.
  • Schlüssel __proto__, constructor und prototype an der Vertrauensgrenze ablehnen. Ein einziger Guard auf der Input- Parsing-Schicht schützt jeden nachgelagerten Merge-Aufruf.

Die eingesetzten Bibliotheken patchen.

  • lodash 4.17.12+, qs 6.9.7+, minimist 1.2.6+, handlebars 4.7.7+, object-path 0.11.5+. npm audit oder pnpm audit ausführen und Advisories rund um Prototype Pollution abarbeiten. Praktisch jede populäre Utility-Library hat einen Fix veröffentlicht.

Den Prototype einfrieren.

  • Verteidigung in der Tiefe: beim Anwendungsstart Object.freeze(Object.prototype) aufrufen. Das verhindert, dass irgendein Code (auch nicht auditierte Dependencies) auf den Prototype schreibt. Bibliotheken, die den Prototype monkey-patchen, brechen damit. Vor der Aktivierung also gründlich testen.

Für Client-Code gilt dasselbe, plus: untrusted Daten nicht per JSON.parse oder Deep-Merge in eigene Konfigurationsobjekte übernehmen. location.hash und Query-Parameter an der Grenze als nicht vertrauenswürdig behandeln.

Wie blueredix Prototype Pollution meldet

Der Scanner meldet CVEs in JavaScript-Bibliotheken Dritter, deren Beschreibung Prototype Pollution nennt, und erkennt verwundbare Versionen bekannter Kandidaten (lodash, minimist, qs, handlebars), die per <script>-Tag eingebunden oder in Sourcemaps sichtbar sind. Den aktiven Versuch, __proto__-Payloads gegen Ihre Anwendung zu schicken, übernehmen wir nicht. Das ist Penetration-Testing- Territorium.

Mehr dazu