JavaScript-Typverwechslung: Eingabevalidierung umgangen (und wie Sie das beheben)
Alessio Della Libera
3. November 2021
0 Min. LesezeitIn einem früheren Blogbeitrag haben wir gezeigt, wie Typmanipulation (oder Typverwechslung) dazu verwendet werden kann, aus Template-Sandboxes auszubrechen und so Schwachstellen wie Cross-Site-Scripting (XSS) oder Code-Injection zu verursachen.
Eines der Hauptziele dieser Recherche war es, im JavaScript-Ökosystem zu untersuchen, wie und ob sich bestimmte Sicherheitskorrekturen oder Eingabevalidierungen durch einen Typverwechslungsangriff umgehen lassen (d. h. durch die Übergabe eines unerwarteten Eingabetyps).
Wir begannen unsere Recherche mit der Untersuchung, wie häufige Sicherheitslücken wie Prototype Pollution und XSS behoben werden. Dafür nutzten wir Daten aus unserer Snyk Intel Vulnerability Database, der branchenweit größten Datenbank für Open-Source-Schwachstellen. Anhand dieser Informationen konnten wir Muster identifizieren, die verschiedene Maintainer verwenden, um bestimmte Klassen von Schwachstellen zu verhindern.
Nun ist es an der Zeit, Typverwechslungsschwachstellen genauer zu betrachten. In diesem Blogbeitrag zeigen wir häufige Szenarien, in denen sich die Eingabebereinigung und -validierung durch die Übergabe eines unerwarteten Eingabetyps umgehen lassen. Außerdem stellen wir Beispiele für Abhilfemaßnahmen und Vorschläge für Open-Source-Maintainer bereit, die solche Umgehungen der Eingabevalidierung beheben möchten.
JavaScript-Grundlagen
Bevor wir besprechen, wie sich ein Array-Wert zum Umgehen bestimmter Eingabevalidierungen und damit möglicherweise zum Auslösen einer Sicherheitslücke verwenden lässt, sehen wir uns zunächst einige grundlegende Konzepte zur Funktionsweise von JavaScript an. Sie werden später sehr hilfreich sein, insbesondere bei der Fallstudie zu Prototype Pollution.
Werte vergleichen: === und ==
In JavaScript gibt es zwei verschiedene Operatoren zum Vergleichen von Werten:
Strikte Gleichheit
===Lose Gleichheit
==
Einer der Hauptunterschiede zwischen ihnen: Der Operator === gibt immer false zurück, wenn die beiden Operanden unterschiedliche Typen haben (es findet keinerlei Typumwandlung statt). Beim Operator == werden Operanden unterschiedlichen Typs zunächst in einen gemeinsamen Typ umgewandelt und dann verglichen. Weitere Informationen zur Funktionsweise dieser Operatoren finden Sie in der offiziellen Dokumentation zum Operator für strikte Gleichheit und zum Operator für lose Gleichheit.
Das folgende Beispiel zeigt dieses Verhalten:
Werden Operanden mit demselben Wert und Typ verglichen, geben sowohl == als auch === den Wert true zurück (Zeilen 2 und 3). Vergleichen wir jedoch ein Array mit einem anderen Operanden, der dieselbe Zeichenfolgendarstellung hat, wird nur dann true zurückgegeben, wenn der Operator == verwendet wird. In Zeile 6 wird der Wert ["test"] zunächst in seine Zeichenfolgendarstellung umgewandelt und dann mit dem anderen Operanden "test" verglichen. Haben die Operanden unterschiedliche Typen, gibt der Operator === immer false zurück (Zeile 7).
Hinweis: Die obigen Überlegungen gelten auch für die Ungleichheitsoperatoren !== und !=.
Mögliche Wege, die Zeichenfolgendarstellung eines Werts zu erhalten:
die Methode
toString()aufrufen (Zeile 2)mit der Funktion
Stringin eine Zeichenfolge umwandeln (Zeile 2)den Wert mithilfe des Operators
+mit einer leeren Zeichenfolge verketten (Zeile 3)
Bei einem Array-Wert besteht seine Zeichenfolgendarstellung aus der durch Kommas getrennten Verkettung seiner Elemente.
Eigenschaftszugriff
Ein weiteres wichtiges Merkmal von JavaScript ist, dass sich beim Zugriff auf Objekteigenschaften mit der Klammernotation Werte verschiedener Typen (nicht nur Zeichenfolgen) angeben lassen. Ist der Wert keine Zeichenfolge (oder kein Symbol), wird er zunächst in eine Zeichenfolge umgewandelt und dann für den Zugriff auf die Objekteigenschaft verwendet.
Sehen wir uns das folgende Beispiel an:
Wie wir sehen, kann der Schlüssel für den Zugriff auf eine Eigenschaft unterschiedliche Typen haben. In Zeile 4 wird die Eigenschaft ["test"] zunächst in eine Zeichenfolge umgewandelt (also in den Wert "test"), und das Ergebnis dieser Operation wird als Schlüssel für den Zugriff auf die Objekteigenschaft verwendet. Wenn wir in Zeile 6 die Zeichenfolge "test" verwenden, erhalten wir den in Zeile 4 festgelegten Wert.
Auf dieselbe Weise können wir mit einem leeren Objekt {} (Zeile 9), dessen Zeichenfolgendarstellung [object Object] lautet, einen Objektwert schreiben und anschließend über die Zeichenfolge [object Object] direkt auf denselben Wert zugreifen (Zeile 11).
Methoden von String und Array
Es gibt einige integrierte Methoden, die sowohl für String als auch für Array definiert sind und denselben Namen haben. Beispiele dafür sind includes, indexOf, lastIndexOf usw.
Warum ist das für diesen Blogbeitrag relevant? Diese Methoden verhalten sich je nach Typ der Eingabe, auf die sie angewendet werden, unterschiedlich. Wird eine dieser Methoden zur Validierung von Benutzereingaben verwendet, kann die Validierung möglicherweise umgangen werden, wenn eine Eingabe eines anderen Typs (zum Beispiel ein Array) übergeben und nicht überprüft wird.
Das folgende Beispiel zeigt, wie sich integrierte Methoden, die sowohl für String als auch für Array definiert sind, unterschiedlich verhalten:
Hinweis: Alle Werte im Beispiel ("<script>", ["<script>"] und [["<script>"]]) haben dieselbe Zeichenfolgendarstellung.
In Zeile 3 wird beispielsweise die Methode String.prototype.indexOf() aufgerufen. Sie prüft, ob das Zeichen < in der Zeichenfolge vorkommt, und gibt dann den Index seines ersten Vorkommens zurück. Andernfalls gibt sie -1 zurück.
Ist die Eingabe jedoch ein Array, wird die Methode Array.prototype.indexOf() aufgerufen. Sie prüft, ob das Element < im Array vorkommt. Da das Array nur ein Element enthält (die Zeichenfolge "<script>"), gibt die Funktion -1 zurück. Dadurch wird eine mögliche Bereinigung dieses Zeichens verhindert.
Zusammenfassung der JavaScript-Grundlagen
Fassen wir die bisherigen Erkenntnisse zusammen:
Haben Operanden unterschiedliche Typen, gibt der Operator
===immerfalsezurück.Beim Zugriff auf Objekteigenschaften lassen sich Schlüssel unterschiedlicher Typen verwenden (bei Verwendung der Klammernotation).
Einige integrierte Methoden, die sowohl für
Stringals auch fürArraydefiniert sind (wieindexOfoderincludes), können sich je nach Eingabetyp unterschiedlich verhalten.
Bevor wir fortfahren, sehen wir uns einige Szenarien an, in denen bestimmte der oben genannten Methoden zur Validierung von Benutzereingaben verwendet werden.
Die folgende Prüfung reicht möglicherweise nicht aus, um zu verhindern, dass der Schlüssel isAdmin für das Objekt obj festgelegt wird (wir gehen davon aus, dass die Variable prop vom Benutzer kontrolliert wird):
Wird der Wert von prop in "isAdmin" geändert, löst das obige Beispiel eine Ausnahme aus.
Das folgende Beispiel zeigt eine weitere „schwache“ Prüfung, mit der potenzielles XSS verhindert werden soll, indem nach gefährlichen Zeichen in der Eingabe gesucht wird:
Wichtig ist, dass diese Umgehungen nur unter bestimmten Bedingungen möglich sind. Beispielsweise muss die Eingabe aus bestimmten Quellen stammen (siehe nächster Abschnitt), und auf die Eingabe dürfen keine integrierten Methoden angewendet werden, die ausschließlich für String definiert sind. Andernfalls gibt die Anwendung einen Fehler bzw. eine Ausnahme zurück (da diese Methoden für andere Typen nicht definiert sind).
Im folgenden Beispiel wird die Methode toLowerCase() auf die Eingabe angewendet. Zwar haben wir bereits gesehen, dass sich der Vergleich === mit einem Array-Wert umgehen lässt; wird jedoch ein Array übergeben, löst die Anwendung eine Ausnahme aus, da die Methode toLowerCase() nur für Zeichenfolgen definiert ist.
Array-Werte erhalten
Vielleicht fragen Sie sich an dieser Stelle, wie sich Array-Werte aus entfernten Daten beziehen lassen.
Es gibt verschiedene Möglichkeiten, Array-Werte zu erhalten:
Serverseitig: Bei Verwendung des beliebten Express-Frameworks können Werte aus
req.queryoderreq.body(wenn die Middlewareexpress.json()verwendet wird) auch als Arrays oder Objekte geparst werden, nicht nur als Zeichenfolgen.Clientseitig: Daten aus der
postMessage-API.
Serverseitig
Bei GET-Anfragen lassen sich bei Verwendung des beliebten Express-Frameworks Array-Werte über die Notation parameter[]=value beziehen, wenn der Wert aus req.query stammt. Allgemein gilt dies für jede Anwendung, die die beliebte Bibliothek qs zum Parsen des querystring verwendet.
Bei POST-Anfragen wird der Request-Body als JSON-Objekt geparst, wenn die Middleware express.json() verwendet wird.
Die folgende Express-Anwendung zeigt, wie sich Array-Werte beziehen lassen:
Clientseitig
Daten aus der postMessage-API können ebenfalls unterschiedliche Typen haben, nicht nur Zeichenfolgen:
Um den obigen Code zu testen, öffnen Sie die Entwicklerkonsole des Browsers und führen Sie Folgendes aus: window.postMessage({name: ["array"]}, "*"). Die folgende Ausgabe sollte angezeigt werden:

Ergebnisse
Im Rahmen dieser Recherche haben wir unter anderem folgende Probleme gefunden und offengelegt:
Modul | Schwachstelle | Snyk Advisory | CVE |
|---|---|---|---|
object-path | Prototype Pollution | CVE-2021-23434 | |
immer | Prototype Pollution | CVE-2021-23436 | |
mpath | Prototype Pollution | CVE-2021-23438 | |
set-value | Prototype Pollution | CVE-2021-23440 | |
edge.js | Cross-Site-Scripting (XSS) | CVE-2021-23443 | |
jointjs | Prototype Pollution | CVE-2021-23444 | |
datatables.net | Cross-Site-Scripting (XSS) | CVE-2021-23445 | |
teddy | Cross-Site-Scripting (XSS) | CVE-2021-23447 |
Wir haben mehrere Maintainer verantwortungsvoll und vertraulich kontaktiert und uns dabei an unsere Vulnerability Disclosure gehalten (einige Funde befinden sich noch im Offenlegungsverfahren).
Vor allem möchten wir allen kontaktierten Maintainern für ihre Zeit danken.
In den folgenden Fallstudien erklären wir, wie sich eine Korrektur gegen Prototype Pollution und eine XSS-Eingabevalidierung durch die Übergabe von Array-Werten umgehen ließen.
Fallstudie: Prototype Pollution in object-path
object-path ist eine Bibliothek, mit der sich über einen Pfad auf tief verschachtelte Eigenschaften zugreifen lässt. Die Bibliothek akzeptiert auch Array-Pfade. Das bedeutet, dass Pfade in der Form ['part1', 'part2', etc.] angegeben werden können. Die Bibliothek war bereits von einer Prototype-Pollution-Schwachstelle betroffen (CVE-2020-15256). Die eingeführte Korrektur lautet:
Diese Korrektur löst korrekt eine Ausnahme aus, wenn der Pfad eine dieser gefährlichen Komponenten enthält (und diese Zeichenfolgen sind). Bei der folgenden Nutzlast löst die Funktion tatsächlich einen Fehler aus:
Wie wir jedoch bereits gesehen haben:
Beim Zugriff auf Objekteigenschaften über die Klammernotation können Schlüssel beliebigen Typs verwendet werden.
Der Operator
===gibtfalsezurück, wenn die Operanden unterschiedliche Typen haben. Die BedingungcurrentPath === '__proto__'gibtfalsezurück, wenncurrentPathden Wert['__proto__']hat (dasselbe gilt fürconstructor).
Das bedeutet: Wird der gefährliche Schlüssel in ein Array mit nur einem Element (dem Schlüssel selbst) eingeschlossen, lässt sich die Korrektur umgehen und es kommt weiterhin zu Prototype Pollution:
Abhilfemaßnahmen
Snyk kontaktierte den Maintainer am 25. August 2021 vertraulich. Das Problem wurde am 27. August 2021 umgehend in v0.11.6 behoben. Die eingeführte Korrektur verhindert dieses Szenario, indem sie die Pfadkomponenten vor der Prüfung in Zeichenfolgen umwandelt (sofern sie nicht vom Typ Number oder String sind):
Die vorherige Payload funktioniert nicht mehr:
Fallstudie: Cross-Site-Scripting (XSS) in edge.js
edge.js ist eine Templating-Engine für Node.js. Diese Bibliothek bietet integrierte Funktionen, die aktiviert werden können, um Sicherheitslücken wie beispielsweise XSS zu verhindern. Laut der Dokumentation wird „die Ausgabe von Interpolationen (der Code innerhalb der geschweiften Klammern) HTML-escaped, um XSS-Angriffe zu verhindern“. Die ursprüngliche Funktion, die für das Escaping gefährlicher HTML-Zeichen zum Schutz vor XSS zuständig ist, lautet:
Wie Sie sehen, wird input nur dann HTML-escaped, wenn es vom Typ string ist. Das bedeutet: Ist die benutzergesteuerte Eingabe vom Typ object (z. B. ein Array) und kein SafeValue, wird sie auch bei Verwendung von {{ }} im Template ohne Escaping zurückgegeben (ohne Fehler oder Ausnahme). Das kann zu einer XSS-Sicherheitslücke führen.
Um zu zeigen, wie sich dies ausnutzen lässt, sehen wir uns die folgende Webanwendung an, die diese Bibliothek als Template-Engine verwendet, um einen benutzergesteuerten Wert in einem Template darzustellen:
Die Datei views/welcome.edge enthält Folgendes:
Wie bereits erwähnt, können Daten aus bestimmten Quellen auch unterschiedliche Typen haben. Wenn Sie die URL http://localhost:3000/test?name=%3Cimg%20src=x%20onerror=%27alert(1)%27%20/%3E aufrufen, escaped die Funktion die Eingabe. Über diese andere URL (achten Sie auf die Klammern []) http://localhost:3000/test?name[]=%3Cimg%20src=x%20onerror=%27alert(1)%27%20/%3E lässt sich jedoch XSS auslösen, da der Parameter req.query.name als Array geparst und deshalb nicht escaped wird.
Behebung
Snyk kontaktierte den Maintainer am 1. September 2021 vertraulich, und das Problem wurde umgehend in v5.3.2 behoben. Der eingeführte Fix berücksichtigt dieses Szenario, indem alle Eingaben escaped werden, die nicht vom Typ SafeValue sind:
Eine mögliche Alternative, um solche Szenarien zu verhindern, besteht darin, die Eingabe vor dem Escaping in einen String umzuwandeln (und so eine Typprüfung wie im obigen Fall zu vermeiden). Alternativ kann eine leere Zeichenfolge (oder eine Fehlermeldung) zurückgegeben werden, wenn die Eingabe nicht vom Typ String ist.
Wichtige Erkenntnisse für …
Entwickler
Wenn Sie den Operator === für eine Bereinigung verwenden, müssen Sie sicherstellen, dass beide Operanden denselben Typ haben. Beim Escaping von Eingaben muss außerdem berücksichtigt werden, dass die Eingabe möglicherweise nicht vom Typ String ist (insbesondere, wenn das Verhalten der Funktion bei Nicht-String-Werten nicht dokumentiert ist).
Maintainer
Wenn eine Funktion oder API für die Bereinigung von Eingaben zuständig ist, aber deren Typ nicht behandelt wird, sollte dieses Verhalten dokumentiert werden, damit es nicht zu Verwirrung kommt. Ein Maintainer behandelt möglicherweise einige Probleme bei der Bereinigung (etwa das Escaping von Eingaben, wenn es sich um Strings handelt), aber nicht andere (den Umgang mit Eingaben, die keine Strings sind).
Sicherheitsforscher
Wir haben uns auf Prototype Pollution und XSS-Sicherheitslücken konzentriert. Wir sind jedoch überzeugt, dass es weitere Fälle und Szenarien geben könnte, in denen sich dieser Angriffsvektor nutzen lässt, um vorhandene Bereinigungsmaßnahmen zu umgehen und potenzielle Sicherheitsprobleme zu verursachen. Wenn Sie ähnliche Probleme (oder andere Sicherheitslücken) in einem Open-Source-Projekt finden, das von unserem Programm unterstützt wird, können Sie diese gerne über das Snyk Vulnerability Disclosure-Formular an uns melden.
Referenzen
Prototype Pollution: Arteau, Oliver. „JavaScript prototype pollution attack in NodeJS application.“ GitHub, 26. Mai 2018
Starten Sie mit Capture-the-Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Challenges lösen.



