5 Möglichkeiten, Code-Injection in JavaScript und Node.js zu verhindern
6. April 2021
0 Min. LesezeitSicheren JavaScript-Code so zu schreiben, dass Code-Injection verhindert wird, mag wie eine ganz normale Aufgabe erscheinen. Doch auf dem Weg dorthin gibt es viele Fallstricke. So bedeutet es beispielsweise nicht, dass andere ebenfalls Best Practices für Sicherheit befolgen, nur weil Sie als Entwickler dies tun. Wahrscheinlich verwenden Sie Open-Source-Pakete in Ihrer Anwendung. Woher wissen Sie, ob diese sicher entwickelt wurden? Was, wenn sich dort unsicherer Code wie eval() befindet? Sehen wir uns das genauer an.
Was ist Code-Injection?
Code-Injection ist eine bestimmte Form umfassenderer Injection-Angriffe. Dabei kann ein Angreifer JavaScript- oder Node.js-Code senden, der vom Browser oder der Node.js-Laufzeitumgebung interpretiert wird. Die Sicherheitslücke entsteht, wenn der Interpreter nicht zwischen dem vertrauenswürdigen, vom Entwickler vorgesehenen Code und dem vom Angreifer als Eingabe bereitgestellten injizierten Code unterscheiden kann.
So verhindern Sie Code-Injection
Als wichtige Konvention für sicheres Programmieren sollten Sie in der Anwendung keine dynamische Codeausführung zulassen. Das bedeutet, dass Sie Sprachkonstrukte wie eval und Code-Strings vermeiden sollten, die an setTimeout() oder den Function-Konstruktor übergeben werden. Vermeiden Sie außerdem Serialisierung, die anfällig für Injection-Angriffe sein könnte, bei denen während des Serialisierungsvorgangs Code ausgeführt wird. Führen Sie schließlich Dependency-Scans durch, um sicherzustellen, dass Ihre Anwendung nicht durch Open-Source-Komponenten von Drittanbietern für diesen Angriff anfällig ist. Wenn Sie zudem ein statisches Codeanalysetool wie Snyk Code verwenden, können Sie potenzielle Sicherheitslücken durch Code-Injection in Ihrem eigenen oder im Code Ihrer Kollegen finden.
In diesem Artikel sehen wir uns 5 Möglichkeiten an, Code-Injection zu verhindern:
Vermeiden Sie
eval(),setTimeout()undsetInterval()Vermeiden Sie
new Function()Vermeiden Sie die Code-Serialisierung in JavaScript
Verwenden Sie einen Node.js-Sicherheitslinter
Verwenden Sie ein statisches Codeanalysetool (SCA), um Probleme mit Code-Injection zu finden und zu beheben
1. Vermeiden Sie eval(), setTimeout() und setInterval()
Ich weiß, was Sie denken: Hier ist noch ein Leitfaden, der mir sagt, ich soll eval vermeiden. Ja, das stimmt. Ich möchte Ihnen aber auch Beispiele aus der Praxis für andere beliebte Bibliotheken zeigen, die eval (oder andere Formen der Codeerstellung) verwendet haben – und sich damit später selbst geschadet und eine schwerwiegende Sicherheitslücke verursacht haben.
Bevor wir uns die Verweise auf anfällige Drittanbieterpakete ansehen, erklären wir zunächst eval und die zugehörigen Funktionen. JavaScript-Laufzeitumgebungen wie Browser und die serverseitige Node.js-Plattform ermöglichen es, Code während der Laufzeit auszuwerten und auszuführen. Ein praktisches Beispiel dafür ist:
Damit versucht ein Programmierer, dynamisch auf Daten im DOM zuzugreifen. In diesem Beispiel wird davon ausgegangen, dass getElementform ebenso wie die Variable elementId potenziell von Benutzern kontrolliert werden kann. Für diese Aufgabe gibt es bessere Möglichkeiten, als eval zu verwenden. Sie sollten dynamischen Code wie diesen daher unbedingt vermeiden.
Auf der Node.js-Seite möchte man möglicherweise anhand einer dynamischen Auswertung den Zugriff auf bestimmte Datenpunkte in der Anwendung ermöglichen. Hier ein Beispiel:
In diesem Beispiel wird allgemein davon ausgegangen, dass die genaue Datei, die wir einbinden möchten, dynamisch und potenziell von Benutzern kontrolliert wird. Auch hier besteht daher die Gefahr von Sicherheitslücken durch Code-Injection.
Code-Injection in Dustjs: ein Praxisbeispiel für die unsichere Verwendung von eval
Das npm-Paket dustjs von LinkedIn – ein asynchrones Template-Projekt für Browser und den serverseitigen Einsatz mit Node.js – zeigt, wie schwerwiegend eine Sicherheitslücke durch Code-Injection werden kann.
Dieses Paket wird zwar nicht mehr gut gepflegt, verzeichnet aber weiterhin rund 72.000 Downloads pro Monat und musste eine Sicherheitslücke durch Code-Injection beheben.
Die Maintainer von dustjs haben ihr Bestes getan, um potenziell gefährliche Benutzereingaben zu maskieren, die in unsichere Codekonstrukte wie die Funktion eval() gelangen könnten. Die Funktion escapeHtml selbst hatte jedoch eine Sicherheitslücke: Sie prüfte lediglich, ob es sich um Zeichenfolgen handelte, und maskierte dann die Eingabe. Sie hätte auch andere Typen wie etwa Arrays prüfen müssen. Dieser Pull Request hat die Sicherheitslücke durch Code-Injection behoben:

Sie fragen sich, welche Rolle eval() dabei spielt?
Wenn Sie dustjs verwenden, nutzen Sie möglicherweise auch das npm-Paket dustjs-helpers, um zusätzliche Template-Helfer wie mathematische und logische Operationen zu erhalten. Einer dieser zusätzlichen Helfer ist eine if-Bedingung, die Sie in Ihren eigenen Dust-Template-Dateien beispielsweise so verwenden könnten:

Klingt logisch, oder?
Das Problem: Die unkontrollierte Benutzereingabe im Abfrageparameter device fließt direkt in den if-Bedingungshelfer ein. Wie Sie in Zeile 227 sehen, verwendet dieser eval, um die Bedingung dynamisch auszuwerten:

Jetzt wird alles klar. Hier kommen mehrere Sicherheitsprobleme auf unvorhergesehene Weise zusammen:
dustjs-linkedin, ein Open-Source-Paket, weist eine Sicherheitslücke auf: Eingabezeichenfolgen werden in der Funktion
escapeHtmlnicht korrekt bereinigt.dustjs-helpers, ein Open-Source-Paket, verwendet eine unsichere Programmierkonvention wie die Funktion
eval(), um Code während der Laufzeit dynamisch auszuwerten.
Möchten Sie sehen, wie ich diese Sicherheitslücke ausgenutzt und eine echte, laufende Anwendung genau auf dieser Grundlage gehackt habe? Sehen Sie sich das an:

Vermeiden Sie auch setTimeout() und setInterval()
Zum Abschluss der Best Practice, eval() zu vermeiden, möchte ich auch auf andere Funktionen hinweisen, von denen Sie als JavaScript-Entwickler mit Sicherheit schon gehört oder die Sie mindestens einmal in Ihrer Anwendung verwendet haben: setTimeout() und setInterval().
Weniger bekannt ist, dass diese Funktionen auch Code-Strings entgegennehmen. Sie können beispielsweise so verwendet werden:
Zum Glück sind String-Literale in einer Node.js-Umgebung nicht zulässig!
2. Vermeiden Sie new Function()
Ein weiteres Sprachkonstrukt, ähnlich den oben genannten Funktionen eval(), setTimeout() und setInterval(), ist der Function-Konstruktor. Damit lässt sich anhand von String-Literalen dynamisch eine Funktion definieren.
Sehen wir uns ein einfaches Beispiel an:
Wenn Sie bis hierher aufmerksam mitgelesen haben, wissen Sie bereits, welche potenziellen Sicherheitsprobleme entstehen können, wenn Benutzereingaben in eine solche Funktion gelangen …
3. Vermeiden Sie die Code-Serialisierung in JavaScript
Serialisierung spielt im Java-Ökosystem eine große Rolle. Mein Kollege Brian Vermeer hat einen Blogbeitrag darüber geschrieben, wie Sicherheitslücken Java-Anwendungen aufgrund unsicherer Serialisierungsvorgänge betreffen. Ich empfehle Ihnen dringend, ihn zu lesen: Serialisierung und Deserialisierung in Java: Die Java-Deserialize-Sicherheitslücke erklärt.
Zurück zu JavaScript: Offenbar spielt Serialisierung auch hier eine Rolle.
Wahrscheinlich schreiben Sie Ihre Serialisierungs- und Deserialisierungslogik nicht selbst. Doch in der wunderbaren Welt von npm stehen Ihnen mehr als 1.500.000 Open-Source-Pakete zur Verfügung – warum sollten Sie sie also nicht nutzen?
js-yaml ist äußerst beliebt und verzeichnet mehr als 28.000.000 Downloads pro Woche. Laut Snyk Advisor weist das Paket insgesamt einen guten Zustand auf:

Wie der obige Screenshot des npm-Pakets js-yaml zeigt, enthielten frühere Versionen jedoch Sicherheitslücken. Welche, fragen Sie?
Versionen von js-yaml waren aufgrund unsicherer Deserialisierung für Codeausführung anfällig. Die Sicherheitslücke entsteht durch die folgende Verwendung des new Function()-Konstruktors:
Sehen wir uns an, wie ein Proof-of-Concept-Exploit für diese Sicherheitslücke aussieht:
Wenn ein Angreifer also eine solche Eingabe oder Teile davon bereitstellen kann, wie sie im obigen Proof-of-Concept-Code zum Erstellen der Variable x verwendet wird, wird aus einer potenziellen Schwachstelle eine reale Gefahr.
Die oben beschriebene Sicherheitslücke stammt aus dem Jahr 2013. Ein Bericht über eine Sicherheitslücke aus dem Jahr 2019 stellte jedoch einen weiteren Fall von beliebiger Codeausführung in js-yaml fest. Seien Sie also vorsichtig – oder, konkreter und praxisorientierter: Vermeiden Sie new Function() und scannen Sie Ihre Open-Source-Pakete von Drittanbietern, um sicherzustellen, dass sie keine solchen Sicherheitslücken enthalten. Falls doch, können Sie diese automatisch mit einem Fix-Pull-Request beheben.
4. Verwenden Sie einen Node.js-Sicherheitslinter
Nun kommen wir zum Teil dieses Leitfadens, in dem es um Tools geht: Linters. JavaScript-Entwickler verwenden gern Linter. Ob Sie standardjs oder eslint einsetzen, um einen Code-Style durchzusetzen – solche Tools sind in JavaScript- und Node.js-Projekten weit verbreitet.
Warum nicht auch Best Practices für Sicherheit durchsetzen? Hier kommt eslint-plugin-security ins Spiel. Wie in der README beschrieben, ist die Verwendung des Plugins ganz einfach. Fügen Sie einfach die folgende eslint-Plugin-Konfiguration hinzu, um die empfohlene Konfiguration zu aktivieren:
Wie hilft der Linter?
Er verfügt über Regeln, die unsichere Programmierkonventionen erkennen, zum Beispiel detect-eval-with-expression, das die Verwendung von eval() mit Ausdrücken oder String-Literalen erkennt. Der Linter bietet außerdem weitere Regeln, etwa zur Verwendung von Node.js-APIs für child_process.
Beachten Sie, dass eslint-plugin-security zuletzt vor über vier Jahren veröffentlicht wurde. Es funktioniert möglicherweise weiterhin einwandfrei, aber Sie sollten auch Nachfolgepakete wie eslint-plugin-security-node in Betracht ziehen.
5. Verwenden Sie ein statisches Codeanalysetool, um Probleme mit Code-Injection zu finden und zu beheben
Statische Codeanalyse (SCA) in Form einfacher Linter wie ESLint ist ein guter Ausgangspunkt. Sie bieten genügend Kontext, um Code-Style-Regeln durchzusetzen. Wie wir beim Node.js-Sicherheitslinter gesehen haben, sind sie jedoch nicht flexibel genug, um Sicherheitsprobleme tatsächlich zu beheben.
Zu den Bedenken, die Entwickler bei einem Node.js-Sicherheitslinter wie eslint-plugin-security haben, gehören:
Fehlalarme: Die Regeln des Linters sind recht einfach und können viele Fehlalarme auslösen, was bei Entwicklern nur zu mehr Frustration und Verwirrung führt. So führt beispielsweise
RegExp(matchEmailRegEx)zu einem Fehler des Node.js-Sicherheitslinters, da die Funktion RegExp nicht mit einem Literal verwendet wird. Vielleicht istmatchEmailRegExaber einfach eine Konstante in meiner Datei shared/variables.js. Der Linter ist nicht ausgereift genug, um das zu erkennen.Zu starre Regeln: Wie bereits erwähnt, ist die Regel zu starr. Entweder Sie verwenden
child_process.exec(someCommand, [])oder Sie tun es nicht. Die statische Codeanalyse mit dem Linter ist nicht intelligent genug, um zu erkennen, dasssomeCommandeine von Ihnen fest codierte Konstante ist. Die Verwendung von child_process.exec() mit einem Nicht-Literal löst bereits einen Linter-Fehler aus. Das frustriert Entwickler, die die Regel am Ende deaktivieren.Zu einfache Regeln: Die Regelsammlung ist zu klein und die Ergebnisse sind zu oberflächlich. Im Grunde gilt: Alles oder nichts – ohne viel Kontext dazu, wie Daten von einer bestimmten Benutzereingabe zu sensiblem Code wie Befehlsausführung, SQL-Abfragen oder anderen Stellen gelangen.
Wie bereits erwähnt, ist ein Sicherheitslinter wie eslint-plugin-security-node oder ein ähnliches Tool ein guter Ausgangspunkt. Das ist definitiv besser als gar nichts.
Doch es gibt bessere Möglichkeiten, Sicherheitsprobleme in Ihrem eigenen Code zu finden – während Sie programmieren.
Lernen Sie Snyk Code kennen: ein Tool für statische Anwendungssicherheitstests (SAST), das speziell für Entwickler entwickelt wurde.
Eine Command-Injection in einer Node.js-Anwendung finden
Snyk Code wird bald veröffentlicht. Ich gebe Ihnen aber schon jetzt einen kleinen Einblick in die Funktionsweise.
Stellen Sie zunächst über ein GitHub-Konto eine Verbindung zu Snyk her und importieren Sie anschließend das GitHub-Repository. Klicken Sie dazu auf Add project und dann auf das GitHub-Symbol:

Wählen Sie anschließend entweder ein Repository aus der Liste aus oder geben Sie es in das Suchfeld ein und aktivieren Sie das Repository, um den Scan zu starten:

Snyk importiert das GitHub-Repository und scannt es anschließend in kürzester Zeit.
Snyk erkennt automatisch weitere Manifestdateien, die auf potenzielle Sicherheitsprobleme hinweisen – zum Beispiel, wenn Sie Open-Source-Abhängigkeiten mit bekannten Sicherheitslücken verwenden oder Ihr Docker-Image zahlreiche Sicherheitslücken enthält.
Konzentrieren wir uns auf unseren eigenen Code in dieser Node.js-Anwendung. Klicken Sie dazu auf Code analysis und sehen Sie sich die Ergebnisse an:

Snyk Code hat mehrere Sicherheitslücken gefunden. Eine davon ist eine Command-Injection-Sicherheitslücke, wie Sie hier sehen:

Die in dieser Codezeile gefundenen Sicherheitsprobleme verdeutlichen das Risiko:
Doch wie gelangen Daten vom Parameter url in die unsichere Funktion exec()? Klicken Sie auf die Schaltfläche full details, um den Datenfluss ausführlicher und mit mehr Kontext anzuzeigen:

Hier sehen Sie deutlich das Gesamtbild, wie Snyk Code es analysiert hat.
Der Parameter url wird aus dem Array item erstellt. Dieses Array ist die Quelle einer benutzergesteuerten Eingabe, die als Nachrichtentext in der Variablen req.body.content übergeben wird.
Beheben der Command-Injection-Sicherheitslücke
Nun können wir weitere Schritte unternehmen, um das Sicherheitsproblem zu beheben, zum Beispiel:
Anstatt die unsichere Funktion
exec()zu verwenden, können wir die sichere Variante dieser API nutzen:execFile(). Sie maskiert die übergebenen Argumente, die als Argument eines Array-Funktionsaufrufs bereitgestellt werden.Wir können und sollten die Variable item aus der Benutzereingabe validieren, maskieren oder bereinigen, bevor sie in sicherheitskritischen Code gelangt, etwa in Code zur Ausführung von Systemprozessen.
Zusammenfassung
Gut gemacht, wenn Sie bis hierher gelesen haben!
Hoffentlich ist Ihnen jetzt bewusster, welche Probleme durch Code-Injection-Sicherheitslücken entstehen können – unabhängig davon, ob sie aus Ihrem eigenen Code oder aus Drittanbieter-Abhängigkeiten stammen, die Sie in Ihre Anwendung einbinden.
Wenn Sie diesen Beitrag hilfreich fanden, finden Sie hier weitere Lektüre von meinen Kolleginnen und Kollegen bei Snyk:
Wenn Sie oder Ihr Team mit Go entwickeln, lesen Sie unseren Go-Sicherheits-Spickzettel: 8 Best Practices für Go-Entwickler.
Wenn Sie mit Java und insbesondere Spring MVC arbeiten, ist dieser Beitrag lesenswert: Java-Sicherheitsprobleme in meiner Spring-MVC-Anwendung beheben – auch darin werden Sicherheitsprobleme mit Snyk Code gefunden.
Starten Sie mit Capture the Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.
