Skip to main content

Sicherheitsbedenken bei einer JavaScript-Sandbox mit dem Node.js-VM-Modul

Artikel von
feature red team blue team

22. Februar 2023

0 Min. Lesezeit

Wurden Sie damit beauftragt, ein Produkt zu entwickeln, das dynamisches JavaScript ausführt, das von Endnutzern stammt? Vielleicht denken Sie, dass sich mit dem Node.js-VM-Modul eine JavaScript-Sandbox erstellen lässt. In diesem Artikel erfahren Sie, warum das keineswegs empfehlenswert ist und welche Sicherheitsrisiken damit verbunden sind.

Von Zeit zu Zeit gibt es Projekte, die die routinemäßige Backend-Entwicklung auf die Probe stellen. APIs? Message Queues? Hoher Verarbeitungs- und Rechenaufwand? Nein. Hier ist eine Backlog-Story, über die Sie nachdenken können:

As a user,
I want to write and execute my own custom JavaScript code,
So that it can run within the platform and provide me feedback.

Das ist etwas vage, denn es geht allgemein um die Ausführung nicht vertrauenswürdigen Codes von Nutzern. Es gibt jedoch konkrete Beispiele aus der Praxis, bei denen dies erforderlich sein kann:

  • Das Produkt replit ermöglicht es Ihnen, in der Cloud zu programmieren und Code in einer benutzerdefinierten IDE auszuführen

  • Verschiedene Plattformen zum Üben und für Vorstellungsgespräche rund um „Leet-Code“ ermöglichen es Ihnen, eigenen Code zu schreiben, etwa um einen vorgegebenen Algorithmus zu implementieren oder Tests dafür zu verfassen.

Hier kommt das Node.js-VM-Modul ins Spiel.

Das Node.js-VM-Modul

Mit dem Kernmodul node:vm können Entwickler dynamisch bereitgestellten Code in V8-Virtual-Machine-Kontexten kompilieren und ausführen. Zunächst denken Sie dabei vielleicht an die JavaScript-Funktion eval, die als Sicherheitsrisiko bekannt ist. Ist das Node.js-VM-Modul sicherer? Schließlich heißt es „virtuelle Maschine“.

Sehen wir uns ein praktisches Beispiel an:

const vm = require("node:vm");

const productExpirationDays = 14;

// This works just fine - it is a custom code that manipulates
// the data in the context and only there.
const userInputCustomJavaScriptCode = "userCustomNickname = 'Johnny Mnemonic';";

const context = { userCustomNickname: "John Nash" };
vm.createContext(context);

vm.runInContext(userInputCustomJavaScriptCode, context);

console.log(context.userCustomNickname);
console.log(context.productExpirationDays);
console.log(productExpirationDays);

Im obigen Codebeispiel wird benutzergenerierter Eingabecode mit benutzerdefiniertem JavaScript (gekennzeichnet durch die Variable userInputCustomJavaScriptCode) gezielt in einem Kontext mit bestimmten Variablen ausgeführt: vm.runInContext(). Das Codebeispiel wird erfolgreich ausgeführt und gibt die folgenden Ergebnisse aus:

Johnny Mnemonic
undefined
14

Angreifer können diese Art von dynamischem JavaScript-Code jedoch missbrauchen, indem sie andere Variablen als die ursprünglich zugewiesenen manipulieren. In diesem konstruierten Node.js-Beispiel legt eine Variable namens productExpirationDays die Ablaufzeit für den Nutzer fest. Was wäre, wenn ein Angreifer den Inhalt seines dynamischen JavaScript-Codes so änderte, dass dieser Wert höher gesetzt wird?

const userInputCustomJavaScriptCode = "userCustomNickname = 'Johnny Mnemonic'; productExpirationMinutes += 60";

Wenn wir das vorherige Beispiel mit Nutzereingaben durch dieses ersetzen würden, das die Ablaufzeit verlängern soll, erhielten wir die folgende Fehlermeldung:

evalmachine.<anonymous>:1
userCustomNickname = 'Johnny Mnemonic'; productExpirationDays += 60
                                        ^

ReferenceError: productExpirationDays is not defined
    at evalmachine.<anonymous>:1:41
    at Script.runInContext (node:vm:141:12)
    at Object.runInContext (node:vm:297:6)

Der Fehler tritt auf, weil im Gültigkeitsbereich des Kontexts für die virtuelle Maschine, in der der dynamische JavaScript-Code ausgeführt wird, keine Variable productExpirationDays definiert ist oder existiert.

Alles in allem scheint es, als hätten wir einen Weg gefunden, JavaScript-Code sicher und dynamisch in einer abgeschotteten JavaScript-Sandbox auszuführen.

Eine unsichere JavaScript-Sandbox

Das Codebeispiel, das die Methoden createContext() und vm.runInContext() des Node.js-VM-Moduls verwendet, war stark vereinfacht. In der Realität nutzen schädliche Nutzereingaben jedoch oft intelligentere, kreativere und effektivere Methoden, um aus der JavaScript-Sandbox auszubrechen.

Sehen wir uns an, wie ein Angreifer unsicheren Code einschleusen kann, der einen Denial-of-Service-Angriff auf eine laufende Anwendung auslöst. Betrachten Sie das folgende Node.js-Skript:

const vm = require("node:vm");

const userInputCustomJavaScriptCode =
  "userCustomNickname = 'Johnny Mnemonic'; while(true) {}";

const context = { userCustomNickname: "John Nash" };
vm.createContext(context);

vm.runInContext(userInputCustomJavaScriptCode, context);

// This will never run:
console.log("Never fear, I is here");

Im obigen Codebeispiel hat der Nutzer seiner Eingabe eine Endlosschleife hinzugefügt: while(true) {}. Der Code verändert zwar keine anderen Anwendungsvariablen, löst aber einen Denial-of-Service-Angriff aus.

Die Risiken einer unsicheren JavaScript-Sandbox umfassen auch die Remote-Codeausführung. Mit this.constructor.constructor können wir auf das JavaScript-Objekt Function verweisen. Eine JavaScript-Funktion kann Code als String entgegennehmen und anschließend ausführen. Unser Angriff nutzt diese Eigenschaft aus und gibt mithilfe einer sofort aufgerufenen Funktionsausführung, kurz IIFE, die Umgebungsvariablen des laufenden Node.js-Prozesses aus:

const vm = require("node:vm");

const userInputCustomJavaScriptCode =
  "this.constructor.constructor('console.log(process.env)')()";

const context = { userCustomNickname: "John Nash" };
vm.createContext(context);

vm.runInContext(userInputCustomJavaScriptCode, context);

console.log("Mess with the best, drop like your envs!");

Bei der Ausführung des obigen Codebeispiels wird die Ausgabe von process.env angezeigt, in der alle Umgebungsvariablen aufgeführt sind.

An diesem Punkt sollten Sie erkennen, welche Folgen Remote-Codeausführung haben kann, wenn nicht vertrauenswürdiger Code im VM-Modul von Node.js ausgeführt wird. Wenn Nutzer eigenen Code ausführen dürfen, haben sie vollständigen Zugriff auf die Node.js-Serverlaufzeit und können Prozesse starten, auf das Dateisystem zugreifen und vieles mehr.

Zusammenfassung

Die Folgen einer unsicheren JavaScript-Sandbox in einer Node.js-Umgebung sind schwerwiegend und können eine ganze Anwendung lahmlegen. Wir haben gesehen, wie ein Angreifer die JavaScript-Sandbox missbrauchen und schädliche Eingaben bereitstellen kann, die eine Node.js-Anwendung beeinträchtigen und eine Denial-of-Service-Schwachstelle verursachen. Die Angriffsfläche beschränkt sich jedoch nicht darauf. Auch Remote-Codeausführung ist möglich. Dadurch können Angreifer eigenen Code in Node.js-Serverumgebungen ausführen und so die gesamte Anwendungsplattform gefährden.

Tatsächlich ist die durch eine JavaScript-Sandbox mit dem Node.js-VM-Modul entstehende Sicherheitslücke nur der Anfang eines größeren drohenden Datendiebstahls. Sobald ein einzelner Node.js-Anwendungsserver kompromittiert ist, können vertrauliche Informationen offengelegt werden, etwa Zugangsdaten für eine Datenbank oder Cloud-Dienste. Diese könnten es einem Angreifer ermöglichen, den Angriff auszuweiten und sich lateral im bereitgestellten Netzwerk zu bewegen.

Daher empfiehlt es sich, das VM-Modul von Node.js nicht als sichere Sandbox für die Ausführung nicht vertrauenswürdigen JavaScript-Codes zu verwenden. Darauf wird in der Node.js-API-Dokumentation ausdrücklich hingewiesen: „Das Modul node:vm ist kein Sicherheitsmechanismus. Verwenden Sie es nicht, um nicht vertrauenswürdigen Code auszuführen.“

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.