Skip to main content

Nach drei Jahren ohne Meldungen tritt erneut eine jQuery-Schwachstelle durch Prototype Pollution auf

Artikel von
jQuery Blog

15. April 2019

0 Min. Lesezeit

Am 26. März 2019 erfuhren wir von einer neuen Sicherheitslücke in derselben beliebten jQuery-Frontend-Bibliothek – fast drei Jahre nach der Offenlegung der letzten jQuery-Sicherheitslücke.

Diese Sicherheitslücke, die als Prototype Pollution bezeichnet wird und sich darin äußert, ermöglicht Angreifern, den Prototyp eines JavaScript-Anwendungsobjekts zu überschreiben. Dadurch können vom Angreifer kontrollierte Eigenschaften in Objekte eingeschleust werden. Dies kann entweder zu einem Denial-of-Service führen, indem JavaScript-Ausnahmen ausgelöst werden, oder den Anwendungscode so manipulieren, dass der vom Angreifer eingeschleuste Codepfad ausgeführt wird.

Im Folgenden sehen Sie ein Proof-of-Concept-Beispiel aus dem ursprünglichen Bericht, den ich im Rahmen der Node.js Security Working Group (WG) auf HackerOne geprüft habe:

let a = $.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'))
console.log({}.devMode); // true

Wie der Code zeigt, wird die erweiterte jQuery-API verwendet, um mehrere Objekte rekursiv zusammenzuführen.

Die Schwachstelle ist zwar nicht ganz einfach auszunutzen, kann aber aufgrund der Beliebtheit von jQuery im JavaScript-Ökosystem zahlreiche Projekte und Nutzer betreffen. Außerdem haben wir bereits reale Prototype-Pollution-Angriffe beobachtet, etwa den Angriff auf mongoose im Dezember 2018.

Das jQuery-Team hat kürzlich eine Fehlerbehebung veröffentlicht, die dieses Sicherheitsproblem in Version 3.4.0 behebt. Wir empfehlen Ihnen dringend, auf diese Version zu aktualisieren.

Rekonstruktion einer verwundbaren Anwendung

Da jQuery hauptsächlich im Frontend eingesetzt wird, sehen wir uns an, wie sich eine Prototype-Pollution-Schwachstelle in einer clientseitigen Anwendung äußert.

Der Angriff beginnt mit einer Nutzereingabe, über die ein Angreifer ein Objekt einschleusen kann, das der Entwickler möglicherweise weder bereinigt noch für eine besondere Behandlung vorgesehen hat.

Angenommen, Sie entwickeln eine Anwendung, in der ein Nutzer JSON-Payloads senden darf, die unverändert gespeichert werden. Vielleicht möchten Sie dem Nutzer diese Möglichkeit geben, damit er die Inhaltsstruktur selbst kontrollieren kann, für die Sie keine Verantwortung übernehmen möchten.

Ein Beispiel für eine Payload, die in einem solchen Fall gesendet würde:

{
  “myProperty” : “a”,
  "__proto__" : { "isAdmin" : true }
}

Nehmen wir an, Sie sollen ein Objekt rekursiv klonen, sind sich aber nicht ganz sicher, wie Sie dabei vorgehen sollen. Wenn Sie bei Google nach „deep clone an object in javascript“ suchen, erscheint diese Antwort als oberstes Suchergebnis auf Stack Overflow:

Bei mehr als viertausend Upvotes und der Auszeichnung als richtige Antwort könnten Sie sehr geneigt sein, das Beispiel zum Deep Copy einfach zu kopieren und einzufügen, um die Arbeit zu erledigen.

var myObject = ‘{ “myProperty” : “a”, "__proto__" : { "isAdmin" : true } }’
var newObject = jQuery.extend(true, {}, JSON.parse(myObject))

Was würden Sie auf Grundlage des Stack-Overflow-Ratschlags erwarten, wenn Sie dieses Codebeispiel ausführen?

  1. Ein myObject zeigt beispielhaft ein serialisiertes Format, das möglicherweise aus einem Datenbankfeld abgerufen wurde.

  2. JSON.parse() und die jQuery-Funktion extend() werden verwendet, um eine Kopie von myObject zu erstellen.

  3. Die neue geklonte Version heißt newObject.

Wenn JSON.parse() eine Eigenschaft namens __proto__ verarbeitet, wie in unserem Beispiel, könnten Sie erwarten, dass die Eigenschaft isAdmin in der übergeordneten Eigenschaft des Objekts auf true gesetzt wird. Tatsächlich wird jedoch ein Objekt mit diesem Eigenschaftsnamen erstellt, wodurch die Funktionsweise der Eigenschaftskette überschrieben wird.

Wird dieses Verhalten von JSON.parse() mit einer unsicheren Deep-Clone-Funktion kombiniert, auch als Zusammenführen von Objekten bezeichnet, gelangt der der Eigenschaft __proto__ zugewiesene Wert in das globale JavaScript-Objekt.

Sehen wir uns vor diesem Hintergrund den folgenden Codeausschnitt und seine Auswirkungen an, um zu verstehen, wie Prototype-Pollution-Angriffe funktionieren:

var myObject = ‘{ “myProperty” : “a”, "__proto__" : { "isAdmin" : true } }’
var newObject = jQuery.extend(true, {}, JSON.parse(myObject))
// if you do console.log({}.isAdmin) you’ll get true returned
// later down the application source code we may try to detect if the user is an admin or not
If (user.isAdmin === true) {
  // do something like load the relevant interface, run an Ajax call, access localStorage, etc
}

Wenn für die aus der Datenbank abgerufene Eigenschaft isAdmin des Nutzerobjekts kein Wert festgelegt ist, ist sie im Nutzerobjekt im Grunde nicht definiert. In diesem Fall muss beim Zugriff auf die Eigenschaft isAdmin in der if-Klausel das übergeordnete Objekt in der Prototypenkette des Objekts user abgefragt werden. Das ist Object, das nun manipuliert wurde und die Eigenschaft isAdmin mit dem Wert true enthält. Obwohl der Entwickler dies nicht beabsichtigt hat, wird der Nutzer dadurch zum Administrator und kann in der Anwendung erheblichen Schaden anrichten.

Weitere Angriffsvektoren untersuchen

Wie das vorherige Beispiel gezeigt hat, kann ein unsicherer rekursiver Merge-Vorgang in Kombination mit der Funktionsweise von JSON.parse zu einer möglichen Manipulation der Prototypenkette führen.

Dies ist jedoch nicht die einzige Möglichkeit, den Prototyp zu verändern. Sehen Sie sich folgenden Code an:

let myObj = {}
myObj[‘__proto__’][‘a’] = ‘a’
console.log(myObj.a)
let newObj = {}
console.log(newObj.a)

In diesem Beispiel:

  1. wird ein neues myObj erstellt.

  2. Über __proto__ wird auf die Prototypenkette zugegriffen und das betreffende Objekt um eine neue String-Eigenschaft erweitert.

Da der Prototyp von myObj tatsächlich ein von uns verändertes JavaScript-Object ist, enthalten auch alle von nun an erstellten Objekte diese Eigenschaft. Das liegt an der Funktionsweise von JavaScript: Wenn das Objekt a keine Eigenschaft newObj hat, greift JavaScript auf den Prototyp von newObj zu, um sie dort zu finden. Dieser Vorgang wird rekursiv fortgesetzt, bis die gesamte Prototypenkette durchsucht wurde. In unserem Fall ist die Eigenschaft vorhanden. Deshalb wird beim Zugriff darauf der String-Wert zurückgegeben.

Vielleicht fragen Sie sich, wie jemand auf die beschriebene Weise den Zugriff auf das Objekt __proto__ einschleusen könnte.

Stellen wir uns vor, wir entwickeln eine Anwendung, die Pakete auf npm auflistet und damit etwas macht, beispielsweise sie in einer ansprechenden Benutzeroberfläche anzeigt.

Dafür benötigen wir wahrscheinlich eine API, die eine Liste von npm-Paketen und den Inhalt ihrer package.json-Dateien sendet:

[
  {
    "cool-package": {
      "license": "MIT",
      "github": "https://github.com/someuser/cool-package"
    }
  },
  {
    "__proto__": {
      "license": "MIT",
      "github": "https://github.com/",
      “toString”: “april fools”
    }
  }
]

Vielleicht denken Sie, diese API sei sicher, weil die Daten direkt von npmjs selbst und den dort bereitgestellten APIs oder von einer anderen vertrauenswürdigen Website stammen und der Angreifer den API-Dienst nicht selbst betreibt. Das wäre jedoch ein Irrtum. Wie Sie wahrscheinlich bemerkt haben, liegen die Paketdetails wie der Name und der Inhalt von package.json letztlich unter der Kontrolle des Nutzers.

Erweitern wir dieses Anwendungsszenario: Angenommen, der Entwickler möchte aus dem Array eine Map erstellen, damit er ganz einfach auf Pakete zugreifen kann, ohne das Array durchlaufen zu müssen, um die Daten abzurufen.

Im Wesentlichen könnte der Entwickler versuchen, wie folgt auf die Daten zuzugreifen:

const pkgLicense = packageList[‘jquery’][‘license’]

Hier sehen Sie ein funktionierendes Beispiel für solchen Code:

let list_of_npm_packages = JSON.parse(`
[
  {
    "cool-package": {
      "license": "MIT",
      "github": "https://github.com/someuser/cool-package"
    }
  },
  {
    "__proto__": {
      "toString": "april fools"
    }
  }
]
`);

list_of_npm_packages.forEach((package) => {
     for (const [propertyKey, objectValue] of Object.entries(package)) {
          for (const [name, value] of Object.entries(objectValue)) {            
            if (!packagesMap[propertyKey]) {
            packagesMap[propertyKey] = {}
            }
            packagesMap[propertyKey][name] = value 
          }
     }
})

Wenn wir an dieser Stelle völlig neue Objekte erstellen und versuchen würden, auf ihre toString-Eigenschaft zuzugreifen, würde der Prototype-Pollution-Angriff sichtbar.

const a = {}
console.log(a.toString)  // prints ‘april fools’
console.log({}.toString) // prints ‘april fools’

Prototype-Pollution-Schwachstellen in freier Wildbahn

jQuery ist nicht das einzige Opfer dieser Art von Schwachstelle. Allein im letzten Jahr haben wir mehr als 20 Prototype-Pollution-Schwachstellen in Browser- und Node.js-Ökosystemen erfasst. Diese Schwachstellen finden sich sowohl in JavaScript-Bibliotheken wie lodash, wo sie aufgrund der Beliebtheit von lodash zahlreiche Frontend-Projekte betreffen können, als auch in Node.js-Backend-Bibliotheken, die JavaScript-Objekte klonen, etwa node.extend und deep-extend.

Am 21. Februar berichtete auch Eran Hammer, wie ein ähnlicher Angriff eine Bibliothek beeinträchtigen kann: Eine Prototype-Poisoning-Schwachstelle kann eine Bibliothek betreffen, indem Daten über joi offengelegt werden, ein beliebtes Validierungspaket, das viele Projekte im Ökosystem nutzen. Die Schwachstelle betraf auch hoek, eine Utility-Bibliothek derselben Entwicklergruppe, die zum hapi-Webanwendungsframework gehört.

Viele dieser Prototype-Pollution-Schwachstellen wurden von Olivier Arteau, auch bekannt als HoLyVieR, im Rahmen einer verantwortungsvollen Offenlegung für das HackerOne-Programm der Node.js Security WG gemeldet. Das Programm kümmert sich um die Reaktion auf Sicherheitsvorfälle und um Sicherheitsprobleme, die das JavaScript-Ökosystem insgesamt betreffen. Oliver hat außerdem einen detaillierten Schwachstellenbericht zu den Auswirkungen von Prototype Pollution veröffentlicht und auf der NorthSec-Konferenz einen realen Fall vorgestellt, der das Node.js-Projekt Ghost CMS betraf.

Zusammenfassung

Abschließend sollten Sie die folgenden Maßnahmen zur Risikominderung und Best Practices befolgen, um Prototype Pollution zu vermeiden:

  • Vergewissern Sie sich, dass Sie sichere Implementierungen für rekursive Merge-Vorgänge verwenden.

  • Erwägen Sie, Objekte ohne Prototyp zu erstellen, beispielsweise mit Object.create(null), um sie vor Prototype-Pollution-Angriffen zu schützen.

  • Vermeiden Sie bei nutzergesteuerten Daten die Verwendung der eckigen Klammernotation – wenn möglich, sollten Sie ganz darauf verzichten. Verwenden Sie für Map-basierte Strukturen stattdessen den Sprachbaustein Map.

Wir bei Snyk schätzen die Security-Community und sind überzeugt, dass die verantwortungsvolle Offenlegung von Sicherheitslücken in Open-Source-Paketen dazu beiträgt, die Sicherheit und Privatsphäre der Nutzer zu gewährleisten. Wenn Sie glauben, eine Schwachstelle in einem Open-Source-Paket entdeckt zu haben, können Sie uns unter https://snyk.io/vulnerability-disclosure kontaktieren. Unser Programm zur verantwortungsvollen Offenlegung schützt sowohl Entwickler als auch die meldenden Sicherheitsforscher und ermöglicht Entwicklern zugleich, sicher von den durch Forscher entdeckten Schwachstellen zu profitieren.

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.