In this article
So verhindern Sie Prototype-Pollution-Schwachstellen in JavaScript
Prototype Pollution ist eine JavaScript-Schwachstelle, die es Angreifern ermöglicht, potenziell schädliche Eigenschaften zu Objektprototypen hinzuzufügen. Wenn benutzerdefinierte Objekte diese Prototypen erben, werden verschiedene nachfolgende Angriffsformen möglich.
In diesem Artikel erfahren Sie mehr über Prototype Pollution, wie sie entsteht, welche Angriffe sie ermöglicht und wie Sie diese verhindern. Außerdem erfahren Sie, wie Snyk-Tools dabei helfen, diese Schwachstelle in Ihrem Code und Ihren Abhängigkeiten zu erkennen und zu beheben.
Was sind Prototypen in JavaScript?
JavaScript verwendet die sogenannte prototypische Vererbung. Ein Prototyp ist ein Objekt mit einer Reihe von Eigenschaften und Funktionen, die von allen Variablen desselben Typs gemeinsam genutzt werden. Wenn in JavaScript eine Variable deklariert wird, greift sie automatisch auf einen oder mehrere Prototypen zu.
Wenn Sie beispielsweise eine numerische Variable const one = 1 definieren, können Sie auf die Funktion toExponential() im Prototyp der Zahl zugreifen. Der Prototyp der Zahl hat seinerseits ebenfalls einen Prototyp: den Objektprototyp. Deshalb können Sie auch bei der Arbeit mit einer Zahl auf hasOwnProperty() zugreifen – eine Funktion, die im Objektprototyp definiert und vom Prototyp der Zahl nicht überschrieben wird.
Wenn eine numerische Variable einen Prototyp hat, der wiederum mit einem anderen Prototyp verknüpft ist, spricht man von einer Prototypenkette. Die Prototypketten aller Typen führen letztlich zum Objektprototyp.
In den meisten anderen Sprachen sind Vererbungsbeziehungen zur Kompilierzeit festgelegt. In JavaScript können sie dagegen zur Laufzeit aktualisiert werden.
Wenn Sie einem Prototyp einer beliebigen Variable eine Eigenschaft oder Funktion hinzufügen, steht diese Eigenschaft oder Funktion allen anderen Variablen desselben Typs in der Anwendung zur Verfügung – sowohl den bereits vor der Aktualisierung erstellten als auch den danach erstellten.
Wenn Sie dem Objektprototyp an der Spitze der Vererbungskette eine Eigenschaft oder Funktion hinzufügen, steht sie allen anderen Variablen zur Verfügung, sofern diese sie nicht überschreiben.
Hier sehen Sie beispielsweise ein einfaches JavaScript-Objekt:
const myFirstObject = {};
Mit dem Schlüsselwort __proto__ können Sie herausfinden, was der Prototyp dieses Objekts ist:
myFirstObject.__proto__Mit dem folgenden Code können Sie ein weiteres leeres Objekt erstellen und anschließend seinem Prototyp eine Eigenschaft hinzufügen:
const mySecondObject = {}
mySecondObject.__proto__.customProperty = "this is a property in the prototype"Was passiert nun, wenn Sie die Prototypen dieser beiden Objekte protokollieren? Sehen Sie sich den folgenden Code an:
console.log(myFirstObject.__proto__)
console.log(mySecondObject.__proto__)Die Ausgabe beider Aufrufe von console.log() ist identisch:
[Object: null prototype] { customProperty: 'this is a property in the prototype' }
[Object: null prototype] { customProperty: 'this is a property in the prototype' }Die Eigenschaft customProperty, die Sie dem Prototyp des zweiten Objekts hinzugefügt haben, erscheint auch im Prototyp des ersten Objekts. Sie wird nun von allen Objekten in Ihrer Anwendung gemeinsam genutzt.
Wenn Sie wie hier ein drittes leeres Objekt hinzufügen und seinen Prototyp anzeigen möchten, verwenden Sie diesen Code:
const myThirdObject = {}
console.log(myThirdObject.__proto__)Sie werden feststellen, dass auch dort customProperty vorhanden ist:
[Object: null prototype] { customProperty: 'this is a property in the prototype' }Wenn Sie für ein Objekt eine eigene Eigenschaft definieren, wird diese Eigenschaft verwendet:
const nonEmptyObject = {
customProperty: "this is a property defined on the object itself"
}
console.log(nonEmptyObject.customProperty) // "this is a property defined on the object itself"Wenn Sie jedoch ein Objekt mit einer darin nicht definierten Eigenschaft verwenden, sucht die JavaScript-Laufzeitumgebung in der Prototypenkette des Objekts nach dieser Eigenschaft und verwendet sie dann:
const anotherEmptyObject = {}
console.log(anotherEmptyObject.customProperty) // "this is a property in the prototype"Was ist Prototype Pollution in JavaScript?
Prototype Pollution ist eine Schwachstelle, über die Angreifer schädliche Eigenschaften und Funktionen in die Prototypen von JavaScript-Anwendungen einschleusen können. Prototype Pollution ist jedoch kein Angriff an sich, sondern ermöglicht verschiedene Angriffe. Am häufigsten zielen Prototype-Pollution-Angriffe auf den Objektprototyp ab, da Angreifer mit schädlichem Code dort die größtmögliche Reichweite erzielen.
Sowohl serverseitiger als auch clientseitiger JavaScript-Code kann für Prototype-Pollution-Angriffe anfällig sein. Auf dem Server ermöglicht Prototype Pollution häufig eine Ausweitung von Berechtigungen, einen Denial-of-Service-Angriff oder die Ausführung von Code aus der Ferne. Clientseitig kann Prototype Pollution den Weg für DOM-XSS ebnen.
Diese Schwachstelle kann in Anwendungen auftreten, die mit JavaScript oder einer in JavaScript kompilierten Sprache geschrieben wurden, insbesondere mit TypeScript. Außerdem kann Prototype Pollution sowohl in Ihrem Code als auch in Ihren Abhängigkeiten vorkommen – auch in bestimmten Versionen beliebter Bibliotheken wie Lodash, collection.js und jQuery.
Stellen Sie sich vor, ein Angreifer verschafft sich Zugriff und kann in Ihrer laufenden JavaScript-Anwendung die folgende Eigenschaft für den Objektprototyp definieren:
isAdmin: true
Wenn ihm das gelingt, kann der Angreifer Ihren Anwendungscode nach unzureichend abgesicherten Prüfungen von Zugriffsebenen durchsuchen. Stützt sich eine Prüfung auf ein JavaScript-Objekt, das den aktuellen Benutzer darstellt, und hat dieses Objekt die Eigenschaft isAdmin: false nicht, findet die Laufzeitumgebung im Prototyp stattdessen isAdmin: true. Von da an kann der Angreifer seine Berechtigungen leicht ausweiten und auf geschützte Bereiche Ihrer Anwendung zugreifen.
Es geht jedoch nicht nur darum, einem Prototyp neue Eigenschaften hinzuzufügen. Ein Angreifer kann auch bereits vorhandene Funktionen im Prototyp überschreiben. Angenommen, ein Angreifer konnte die Funktion toString() des Objektprototyps durch einen rekursiven Aufruf überschreiben:
let newObject = {}
newObject.__proto__.toString = function() {this.toString()}Danach führt der erste Aufruf von toString() für ein Objekt – oder sogar die Interpolation eines Objekts in einem Template-String – dazu, dass der Hostprozess mit einem RangeError: Maximum call stack size exceeded error abstürzt.
Eine gängige Methode, Prototypen zu manipulieren, sind unsichere Implementierungen rekursiver Eigenschaftenzusammenführungen im Anwendungscode und in Bibliotheken.
Prototype Pollution in der Praxis
Experimentieren wir mit einer Beispielanwendung, damit Sie verstehen, wie Prototype Pollution in Node.js funktioniert. Sie arbeiten mit einer Express-Anwendung, einer MongoDB-Datenbank im Arbeitsspeicher und einigen Endpunkten.
Klonen Sie dieses Repository, wenn Sie den Schritten folgen möchten. Stellen Sie anschließend die Abhängigkeiten wieder her und starten Sie die Anwendung:
npm install
npm startRufen Sie in Ihrem Browser http://localhost:8080/todos/ auf oder senden Sie mit Ihrem bevorzugten HTTP-Client eine GET-Anfrage an http://localhost:8080/todos/. Sie erhalten ein Array mit drei To-do-Einträgen:
[
{
"_id": "654c062468efa31d8c055c13",
"text": "Jason's first todo item",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c0e",
"__v": 0
},
{
"_id": "654c062468efa31d8c055c14",
"text": "First todo for Kelly",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c0f",
"__v": 0
},
{
"_id": "654c062468efa31d8c055c15",
"text": "Paul's thing to be done",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c10",
"__v": 0
}
]
Wenn Sie nun im geklonten Repository die Datei database/seed.js öffnen, sehen Sie, dass die Datenbank der Anwendung nicht mit drei, sondern mit vier To-do-Einträgen initialisiert wurde:
const seedTodoItems = [
{
text: "Jason's first todo item",
open: true,
visible: true,
owner: "Jason"
},
{
text: "First todo for Kelly",
open: true,
visible: true,
owner: "Kelly"
},
{
text: "Paul's thing to be done",
open: true,
visible: true,
owner: "Paul"
},
{
text: "A HIDDEN TODO",
open: true,
owner: "Alexandra"
}
]
Drei To-do-Einträge aus den Seed-Daten sind ausdrücklich als sichtbar festgelegt, einer jedoch nicht und bleibt daher standardmäßig verborgen. Wenn Sie server.js öffnen und sich den Endpunkt für die GET-Anfrage ansehen, erkennen Sie, dass nur sichtbare To-do-Einträge zurückgegeben werden:
app.get('/todos/', (req, res) => {
TodoItem.find({visible: true})
.then(data => res.json(data))
.catch(error => res.json({error}))
})Verwenden Sie einen anderen Endpunkt, um einen neuen To-do-Eintrag hinzuzufügen. Senden Sie dazu die folgende POST-Anfrage an http://localhost:8080/todos/add:
POST http://localhost:8080/todos/add
Content-Type: application/json
{
"__proto__": {
"visible": true
},
"text": "😈 new todo item that tries to mess with the prototype",
"owner": "654c0cc960d82b5802a0b30d"
}Mit der folgenden curl-Anfrage können Sie über die Befehlszeile einen neuen To-do-Eintrag erstellen:
curl -X POST -H 'content-type: application/json' http://localhost:8080/todos/ad
d --data '{ "__proto__": {
"visible": true
},
"text": "😈 new todo item that tries to mess with the prototype",
"owner": "654c0cc960d82b5802a0b30d"
}'Senden Sie nun erneut eine GET-Anfrage an http://localhost:8080/todos/. Folgendes wird zurückgegeben:
[
{
"_id": "654c0d59654aeeb63dfd5ae8",
"text": "Jason's first todo item",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae3",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5ae9",
"text": "First todo for Kelly",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae4",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5aea",
"text": "Paul's thing to be done",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae5",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5aeb",
"text": "A HIDDEN TODO",
"open": true,
"owner": "654c0d59654aeeb63dfd5ae6",
"__v": 0
},
{
"_id": "654c0e6f654aeeb63dfd5aef",
"text": "😈 new todo item that tries to mess with the prototype",
"open": true,
"owner": "654c0cc960d82b5802a0b30d",
"__v": 0
}
]
Zu Beginn gab es drei sichtbare To-do-Einträge. Sie haben einen weiteren hinzugefügt. Beachten Sie aber, dass es jetzt fünf statt vier Einträge sind. Der To-do-Eintrag, der eigentlich verborgen bleiben sollte, ist nun sichtbar!
Sehen wir uns an, was im Endpunkt passiert, der einen neuen To-do-Eintrag hinzufügt:
app.post('/todos/add', (req, res) => {
const defaults = {
open: true,
}
const todoToAdd = lodash.merge(defaults, req.body)
const todoItem = new TodoItem(todoToAdd);
todoItem.save()
.then(() => res.json({msg: `Successfully added a todo item`}))
.catch(error => res.json({error: error.message}))
})
Die Anwendung parst den Body der POST-Anfrage (ein JSON-Objekt) und führt ihn mit dem Objekt defaults zusammen, das für jeden neuen To-do-Eintrag verwendet wird. Für diese Zusammenführung nutzt sie die Methode merge() aus einer anfälligen Version von Lodash. Dadurch wird der schädliche Teil der Anfrage, der den Objektprototyp manipuliert, problemlos in das Ergebnisobjekt übernommen. Dabei wird Object.__proto__.visible auf true gesetzt.
Infolgedessen beziehen alle vorhandenen und zukünftigen Objekte, die keine eigene Eigenschaft visible deklarieren, diese Eigenschaft von ihrem Prototyp. Bei der Datenbankabfrage im Rahmen einer GET-Anfrage verwendet die Anwendung einen Filter, der nur sichtbare To-do-Einträge zurückgeben soll:
app.get('/todos/', (req, res) => {
TodoItem.find({visible: true})
.then(data => res.json(data))
.catch(error => res.json({error}))
})
Bei der Auswertung des Filters zeigt sich, dass der eine verborgene To-do-Eintrag keine eigene Eigenschaft visible: false hat:
{
text: "A HIDDEN TODO",
open: true,
owner: "Alexandra"
}Der Prototyp hat jetzt jedoch die Eigenschaft visible: true. Der Eigenschaftswert des Prototyps wird verwendet. Dadurch wird unbeabsichtigt ein bereits vorhandener To-do-Eintrag offengelegt, der verborgen bleiben sollte. Zusätzlich wird der To-do-Eintrag sichtbar, der Teil der schädlichen Anfrage war, obwohl er selbst keine Eigenschaft visible: true hat.
Prototype-Pollution-Schwachstellen verhindern
In der vorherigen Beispielanwendung lässt sich die Schwachstelle ganz einfach beheben: Aktualisieren Sie Lodash auf eine neuere Version. Es gibt jedoch noch andere Möglichkeiten, Prototype Pollution zu verhindern. Diese sollten Sie kennen – insbesondere, wenn es um Ihren eigenen Code und nicht um Code aus einer Bibliothek geht.
Eigenschaftsschlüssel bereinigen
Am naheliegendsten ist es, vor dem Zusammenführen zu prüfen, ob ein Schlüssel sicher ist:
if (key == '__proto__') {
return;
}Eine Sperrliste zu pflegen, ist jedoch nicht ideal, da es bekannte Möglichkeiten gibt, diesen Schutz zu umgehen.
Besser ist es, eine Zulassungsliste mit erlaubten Eigenschaftsschlüsseln zu pflegen:
if (allowedKeys.includes(key)) {
// Proceed with merge
}Map statt Objekt verwenden
JavaScript bietet eine Alternative zum Speichern von Schlüssel-Wert-Paaren: die Map. Obwohl sie einem Objekt in vielerlei Hinsicht ähnelt, gibt es einen wichtigen Unterschied: Wenn Sie die Funktion get() einer Map verwenden, können Sie nur Werte abrufen, die Sie ausdrücklich in der Map gespeichert haben. Bösartige Elemente, die möglicherweise in den Objektprototyp eingefügt wurden, werden nicht abgerufen:
const defaultsMap = new Map()
defaultsMap.set("open", true);
defaultsMap.set("hidden", false)
// vs
const defaultsObject = {
open: true,
hidden: false
}Änderungen mit Object.freeze() oder Object.seal() verhindern
JavaScript bietet zwei integrierte Funktionen, die Änderungen an Objekten einschränken: Object.freeze() und Object.seal(). Da der Objektprototyp selbst ein Objekt ist, können Sie diese Funktionen auch auf ihn anwenden:
Object.freeze(Object.prototype)Um Prototype Pollution zu verhindern, ist Object.freeze() vorzuziehen, da die Funktion sowohl das Hinzufügen neuer als auch das Ändern vorhandener Eigenschaften eines Objekts verhindert.
Object.seal() verhindert nur das Hinzufügen neuer Eigenschaften, lässt aber Änderungen an bestehenden Eigenschaften zu. Bei Verwendung von Object.seal() bleibt daher die Möglichkeit bestehen, den Objektprototyp durch das Überschreiben seiner Funktionen – etwa toString() – zu manipulieren.
Das Snyk-nopp-Paket verwenden
Eine praktische Erweiterung des Konzepts, Objekte einzufrieren, ist das Snyk-nopp-Paket. Es wendet Object.freeze() auf einige der integrierten JavaScript-Objekte an. Wenn Sie es gegen Ende der Initialisierung Ihrer Anwendung einsetzen, sind legitime Änderungen an Prototypen weiterhin möglich. Böswillige Versuche, Prototypen während des laufenden Betriebs Ihrer Anwendung zu manipulieren, werden dagegen blockiert.
In der zuvor verwendeten Express-Beispielanwendung ist die Einführung von nopp so einfach wie dessen Installation:
npm install noppFügen Sie es anschließend als letzte Import-Anweisung zu server.js hinzu:
import express from 'express';
import connect from "./database/connection.js";
import seedDatabase from "./database/seed.js";
import TodoItem from "./model/todoitem.model.js";
import lodash from "lodash";
import cors from "cors";
import "nopp";Sobald nopp eingeführt ist, wird das Szenario der Prototype Pollution zur Ausweitung von Berechtigungen entschärft – selbst wenn die Beispielanwendung weiterhin eine anfällige Lodash-Version verwendet.
Prototype-Pollution-Schwachstellen mit der Snyk-IDE-Erweiterung erkennen
Es ist wichtig zu beachten, dass sich Schwachstellen – einschließlich Prototype Pollution – leichter beheben lassen, wenn sie einfach zu erkennen sind. Ein Tool, das in Ihrem Code-Editor auf mögliche Sicherheitsprobleme hinweist, kann viel dazu beitragen, dass Sie sicheren JavaScript-Code bereitstellen.
Wenn Sie ein solches Tool interessant finden, installieren Sie die Snyk Security-Erweiterung für Visual Studio Code. (Wenn Sie JetBrains-IDEs wie WebStorm verwenden, gibt es auch für Sie eine Erweiterung.)
Die Snyk-Erweiterung erkennt und markiert mögliche Fälle von Prototype Pollution sowie andere Arten von Schwachstellen in Ihrem JavaScript-Code und Ihren Bibliotheken. Für jede erkannte Schwachstelle zeigt sie, wie verschiedene Open-Source-Projekte ähnliche Probleme behoben haben.
Der folgende Screenshot zeigt die Snyk-IDE-Erweiterung für VS Code in Aktion. Sie erkennt mehrere Schwachstellen im npm-Paket lodash, darunter Prototype Pollution, Denial-of-Service-Angriffe durch reguläre Ausdrücke und Command Injection:

Um die Snyk-Erweiterung zu installieren, suchen Sie im Bereich „Erweiterungen“ von Visual Studio Code nach „snyk“:

Installieren Sie die Erweiterung „Snyk Security – Code, Open Source Dependencies, IaC Configurations“:

Die Snyk-Erweiterung wird zusammen mit der Snyk CLI installiert, die Sie künftig zum Ausführen von Snyk-Analysen benötigen.
Klicken Sie nun in der linken Menüleiste von Visual Studio Code auf das Snyk-Symbol und im Snyk-Bereich auf „Workspace vertrauen und verbinden“:

Snyk öffnet ein Browserfenster, in dem Sie sich bei Ihrem Snyk-Konto anmelden oder ein Konto erstellen müssen:

Nach der Anmeldung muss Snyk Ihren Computer authentifizieren, um Ihre lokale Snyk CLI mit Ihrem Snyk-Konto zu verknüpfen:

Nachdem Sie auf „Authentifizieren“ geklickt haben, bestätigt die Snyk-Webanwendung, dass die Authentifizierung erfolgreich war:

Nun können Sie das Browserfenster schließen und zu Visual Studio Code zurückkehren. Sobald Sie dort einen Workspace oder Ordner öffnen, beginnt Snyk mit der Analyse auf Sicherheitslücken.
So könnte Ihre Visual Studio Code-Ansicht aussehen, nachdem die Snyk-Erweiterung ein Projekt analysiert hat:

Im Bereich „SNYK“ auf der linken Seite sehen Sie eine Liste erkannter Sicherheits- und Codequalitätsprobleme, darunter Prototype Pollution, Regular Expression Denial of Service (ReDoS), Cross-Site Request Forgery (CSRF) und die Offenlegung von Informationen.
Im Tab „Editor“ sind betroffene Codezeilen unterstrichen. Wenn Sie Strg +. (oder auf dem Mac Cmd + .) drücken, wird ein Menü mit passenden Schnellaktionen angezeigt. In einem separaten Bereich fasst die Snyk-Erweiterung die erkannte Sicherheitslücke zusammen und zeigt Beispiele dafür, wie ähnliche Sicherheitslücken in verschiedenen Open-Source-Projekten behoben wurden.
Die Flexibilität von JavaScript hat ihren Preis. Prototype Pollution zeigt besonders deutlich, wie leicht JavaScript missbraucht werden kann, wenn Entwicklerinnen und Entwickler nicht die nötigen Maßnahmen zum Schutz ihres Codes ergreifen.
Am besten lernen Sie Anwendungssicherheit durch praktische Programmierübungen. Entdecken Sie die interaktive Snyk-Learn-Lektion zu Prototype Pollution mit Codebeispielen!

Mit den Snyk-Security-IDE-Erweiterungen können Sie Prototype Pollution und andere Sicherheitslücken frühzeitig erkennen, ohne Ihren bevorzugten Code-Editor verlassen zu müssen.
Wenn Sie die sichereren Funktionen von JavaScript nutzen und auf intelligente Entwicklertools setzen, können Sie sicheren JavaScript-Code bereitstellen und kostspielige Sicherheitsvorfälle vermeiden.
Sichern Sie Ihre JavaScript-Apps ab sofort
Finden und beheben Sie JavaScript-Schwachstellen kostenlos mit Snyk.
Keine Kreditkarte erforderlich.
Oder registrieren Sie sich mit Azure AD Docker ID Bitbucket
Mit der Nutzung von Snyk erklären Sie sich mit unseren Richtlinien einverstanden, einschließlich unserer Nutzungsbedingungen und Datenschutzerklärung.