
TC39 proposal for mitigating prototype pollution
Autoren: Santiago Díaz (Google)
Befürworter: Shu-yu Guo (Google)
Stufe: 1
freeze, seal und preventExtensions
Dieser Vorschlag zielt darauf ab, eine sprachbezogene Schwachstelle namens Prototype Pollution mit einem Mechanismus zu entschärfen, der die Einfrier-Operationen ergänzt, sowie mit einem Mechanismus, der die meisten Codebasen damit kompatibel macht. Er beschreibt eine Opt-in-Funktion, die Prototypen nur noch über Reflexions-APIs zugänglich macht. Dadurch kann die Anweisung obj[key] nicht mehr auf Prototypen zugreifen. Codebasen, die mit dieser Funktion kompatibel sind, gehen bewusster mit der Verwendung von Prototypen um.
PP-Schwachstellen erlauben Angreifern, Objekte zu manipulieren, die sie zur Laufzeit nicht kontrollieren oder auf die sie keinen Zugriff haben. Dieser Mechanismus der 'spukhaften Fernwirkung' kann verwendet werden, um die Form anderer Objekte zu verändern und deren Eigenschaften zu überschreiben, wodurch Objekte zur Laufzeit verunreinigt werden.
Verunreinigte Objekte entkräften die zugrunde liegenden Annahmen von Code, der andernfalls sicher/korrekt wäre, und können zu beliebiger Codeausführung und einer Vielzahl weiterer Sicherheitsprobleme in JS-Codebasen führen. Prototype-Pollution-Fehler äußern sich häufig in Webanwendungen, betreffen aber auch Nicht-Web-JS-Laufzeiten.
Objekteigenschaften in JS sind von jedem Code beschreibbar, der auf sie verweisen kann. Wenn insbesondere viele Objekte von einer gemeinsamen Eigenschaft abhängen, kann jedes einzelne von ihnen Änderungen an allen anderen bewirken.
Eine besondere Eigenschaft von PP ist, dass es sich um einen Data-only-Angriff handelt, der Codeausführung rein durch Daten erreicht. Siehe zum Beispiel den folgenden verwundbaren Code und einen entsprechenden Exploit:
// source is attacker-controlled
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if(target[key] === undefined) {
target[key] = {};
}
target[key] = merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// User input comes as a string
const userSuppliedObj = JSON.parse('{"__proto__": {"polluted": true}}');
// Trigger prototype pollution
merge({}, userSuppliedObj);
// Create a brand new object
const newObj = {};
// Has polluted property
console.log(newObj.polluted); // true
Beachten Sie, dass der Exploit die Erstellung neuer Objekte verunreinigen kann, ohne fremden Code einzuschleusen.
Aufgrund dieser besonderen Eigenschaft greifen moderne Gegenmaßnahmen gegen Codeausführungsprobleme - wie die Content Security Policy oder Trusted Types - bei PP zu kurz, da sie sich auf die Durchsetzung der Codeherkunft konzentrieren.
Hinweis: Data-only-Angriffe sind relevant für Situationen, in denen der auf der VM ausgeführte Code vertrauenswürdig ist und beliebige Codeausführung sicherheitsrelevante Auswirkungen hat.
freeze, seal und preventExtensionsBestehende Einfrier-Operationen leiden unter erheblichen Designproblemen, die eine breite Einführung unwahrscheinlich machen. Sie können für erfahrene Benutzer nützlich sein, sind aber nicht für den Einsatz durch die Mehrheit der Entwickler geeignet, die berechtigterweise erwarten, dass Prototypen veränderbar sind:
Freeze-APIs leiden unter dem Override-Fehler und weiteren Inkonsistenzen, die in bestehenden Codebasen Fehler verursachen, indem sie Werfen auslösen oder schlimmer noch, im Sloppy-Modus still fehlschlagen. Eine frühere Untersuchung des Override-Fehlers kam zu dem Schluss, dass der Override-Fehler bei ~10 % der Codebasen im Strict-Modus und bei 20 % im Sloppy-Modus ausgelöst wird. Die Untersuchung wurde kurz darauf eingestellt.
Freeze-APIs geben Entwicklern die schwere Verantwortung zu wissen, welche Prototypen eingefroren werden müssen, um eine sichere Codebasis zu erhalten – unter der Annahme, dass Entwickler Sicherheitsexperten sind. Diese APIs beschreiben das Was, aber nicht das Wie von Sicherheit. Object einzufrieren ist sicherlich nicht gut genug, da viele Exploits Array missbrauchen. Was ist mit Error, Date, Reflect oder Proxy? Oder zukünftigen eingebauten Typen? Freeze-APIs geben auf diese Fragen keine Antworten.
Freeze-APIs setzen einen stabilen Einfrierpunkt voraus: einen festen Zeitpunkt zur Laufzeit, an dem sich Prototypen beruhigt haben und eingefroren werden können. In der Praxis ist dieser Punkt volatil und ändert sich im Laufe der Zeit in Codebasen, die aktiv weiterentwickelt werden. Während man einen solchen Punkt heute in vielen Anwendungen finden kann, machen das Hinzufügen neuer Abhängigkeiten, Polyfills, Änderungen an der Codestruktur sowie leistungsstarke Funktionen wie Hot-Swapping und Entwicklerwerkzeuge Einfrierpunkte zu einem beweglichen Ziel.
Freeze-APIs können nicht die gesamte Prototypenkette schützen. In JS können Objekte zu jedem Zeitpunkt zur Prototypenkette hinzugefügt oder aus ihr entfernt werden. Um die gesamte Kette zu schützen, muss man stets daran denken, Objekte einzufrieren, die zur Kette hinzugefügt werden – ein fehleranfälliger Prozess. Wenn sie aus der Kette entfernt werden, können sie nicht wieder aufgetaut werden.
Kurz gesagt: eine Funktion, die Prototypen nur über Reflexions-APIs zugänglich macht. Wären Prototypen nicht über Eigenschaften wie __proto__ oder prototype verfügbar, wären sie keinen Data-only-Angriffen ausgesetzt.
Dies wird anhand eines Beispiels besser verständlich: Die Anweisung obj[one][two] = value ist über obj.__proto__.polluted anfällig für PP. Wenn man die Eigenschaft Object.prototype.__proto__ löscht, ist dieselbe Anweisung nicht mehr anfällig, da sie nicht den einzigen anderen Weg zu Prototypen nutzen kann, nämlich obj.constructor.prototype.polluted. Beachten Sie, dass prototype nicht gelöscht werden kann.
Dieser Vorschlag kann umgesetzt werden, indem Reflexions-APIs bereitgestellt und eine neue Opt-in-Kapselungsfunktion geschaffen werden, die Prototypeigenschaften löscht. Eine Beschreibung der einzelnen Schritte folgt.
__proto__ ist ein veralteter Eigenschaftsname, der gelöscht werden kann, aber der interne Slot dahinter kann weiterhin über Object/Reflect.getPrototypeOf gelesen und über Object/Reflect.setPrototypeOf geschrieben werden, wodurch diese Eigenschaft weiterhin für bereits laufenden Code zugänglich bleibt.
Wir schlagen die Schaffung neuer APIs für prototype vor, zum Beispiel getClassPrototypeOf und setClassPrototypeOf, die es ermöglichen würden, diesen Eigenschaftsnamen zu löschen, ohne die Funktionsweise dieser speziellen Eigenschaft und ihre Unterstützung der VM in irgendeiner Weise zu verändern.
Reflexions-APIs können per Polyfill nachgerüstet werden, sodass gehärtete Codebasen in allen Browsern, einschließlich älterer Versionen, funktionieren.
Eine neue Opt-in-'Kapselungsfunktion', bei der keine Eigenschaftsnamen für die Getter- und Setter-Funktionen der Prototyp-Slots erzeugt werden, was nun möglich ist, weil Referenzen auf diese Eigenschaften stattdessen Reflexions-APIs verwenden können.
Die Funktion wird über ein Out-of-Band-Flag aktiviert:
X-Encapsulate-Prototype: true--encapsulate-prototypeWenn Kapselung deaktiviert ist, sind Prototypen sowohl über Eigenschaften als auch über Reflexions-APIs verfügbar.
Wenn Kapselung aktiviert ist, sind Prototypen nur über Reflexions-APIs verfügbar, da sowohl __proto__ als auch prototype gelöscht wurden.
Kapselung umfasst außerdem die folgende Funktion zur automatischen Refaktorierung:
Wenn Kapselung aktiviert ist, führen JS-Engines beim Laden von neuem Quellcode einen zusätzlichen Schritt in ihrer Parse-Phase aus, der alle Punktnotation-Zugriffe auf Prototypeigenschaften so registriert, als wären es Aufrufe ihrer Reflexions-APIs. Dieser Schritt kann effizient implementiert werden und ermöglicht es Codebasen mit Drittanbieter-, transitiven oder dynamisch geladenen Abhängigkeiten, mit der Kapselung kompatibel zu sein.
In Zukunft wird diese Änderung den Weg dafür ebnen, prototype als veraltet (deprecated) zu kennzeichnen.
Prototypeigenschaften könnten bei aktivierter Kapselung einfach undefined sein, aber sie könnten einen Fehler werfen, wenn Lese-/Schreibversuche unternommen werden. Das würde ein schnelleres und lauteres Scheitern bedeuten und Migrationen zu den Reflexions-APIs/der Kapselung testbar machen.
Dies bedeutet, die Getter und Setter von __proto__ und prototype mithilfe eines Host-Hooks von der Kapselung abhängig zu machen, so wie die eval-Funktion unter einer Content Security Policy wirft.
Code, der für den Zugriff auf Prototypen auf berechnete Eigenschaftszugriffe angewiesen ist, ist weder mit der Kapselung noch mit der automatischen Refaktorierung kompatibel. Er muss umstrukturiert werden, um bei der Verwendung explizit auf Prototypen zu verweisen. Diese Refaktorierung lässt den Code tatsächlich seine Absicht ausdrücken, wodurch gefährliche Muster für die statische Analyse sichtbar werden. In der Praxis handelt es sich bei Codebasen mit dieser Eigenschaft in der Regel um Reflexions-Frameworks, Debugging-Werkzeuge und andere reflexionslastige Anwendungsfälle, denen höchstwahrscheinlich bewusst ist, wie sie Prototypen verwenden.
Codebasen, die das Wort prototype verwenden, um benutzerdefinierte Eigenschaften zu definieren, sind nicht kompatibel. Solche Codebasen können mit der Kapselung kompatibel gemacht werden, wenn diese Eigenschaft immer über die Klammernotation gesetzt/abgerufen wird. Historisch gesehen und basierend auf HTTP-Archive-Abfragen gibt es einen kleinen Prozentsatz von Codebasen, die aus diesem Grund inkompatibel sind.
Einige Änderungen an der constructor-Eigenschaft können ebenfalls eine spukhafte Fernwirkung haben. Im Rahmen unserer Forschung haben wir keine praktischen Schwachstellen gefunden, die davon betroffen sind.
Die Hürde für das Funktionieren dieses Angriffs ist beträchtlich hoch: Wie bei PP muss man eine Anwendung mit Gadgets finden, die sowohl beliebige Eigenschaften schreiben als auch lesen können. Doch bei Constructor Pollution muss das Lese-Gadget von constructor.polluted statt von polluted lesen. Das reduziert die Anzahl nützlicher Gadgets erheblich.
Einige minimierte JS-Dateien können mit dem Kapselungsmodus inkompatibel sein, da statische Eigenschaftszugriffe durch Minifizierung zu berechneten Zugriffen werden könnten. Wir haben das HTTP Archive abgefragt, um eine Schätzung dafür in der Praxis zu erhalten. Die folgende Tabelle zeigt, dass Seiten mit diesem Verhalten in den letzten 12 Monaten bei allen mit einem Desktop-Browser gecrawlten Seiten durchgehend unter 1 % liegen:
Google verzeichnet einen Aufwärtstrend bei Fehlern, die über unser Vulnerability-Rewards-Programm eingereicht wurden: 1 im Jahr 2020, 3 im Jahr 2021 und bisher 5 im Jahr 2022. Wir haben in unserer internen Forschung mehrere weitere identifiziert.
Beispiel-Schwachstellen umfassen:
Wir erwarten, dass die Zahl verwundbarer Anwendungen wachsen wird, da JavaScript-Anwendungen in immer mehr Umgebungen eingesetzt werden (z. B. Electron, Cloudflare Workers usw.). Daher ist eine Lösung auf Sprachebene erforderlich, um Angriffe in allen Umgebungen abzuwehren.
| Tabelle | Dokumente, die dynamisch auf __proto__ oder constructor zugreifen | Gesamtzahl der gecrawlten Dokumente | Verhältnis |
|---|
| 2023_03_01_desktop | 5,407,936 | 609,469,458 | 0.89% |
| 2023_02_01_desktop | 4,842,383 | 549,089,708 | 0.88% |
| 2023_01_01_desktop | 5,283,826 | 589,519,160 | 0.90% |
| 2022_12_01_desktop | 5,161,471 | 577,073,883 | 0.89% |
| 2022_11_01_desktop | 5,023,169 | 561,726,239 | 0.89% |
| 2022_10_01_desktop | 4,393,377 | 476,880,624 | 0.92% |
| 2022_09_01_desktop | 4,239,257 | 466,278,762 | 0.91% |
| 2022_08_01_desktop | 4,259,814 | 463,784,047 | 0.92% |
| 2022_07_01_desktop | 3,011,137 | 339,468,615 | 0.89% |
| 2022_06_01_desktop | 2,301,317 | 257,501,222 | 0.89% |
| 2022_04_01_desktop | 2,368,577 | 263,144,657 | 0.90% |
| 2022_03_01_desktop | 2,319,518 | 259,249,013 | 0.89% |