Sandboxed JavaScript inside JavaScript - a proof of concept

In diesem Artikel soll es thematisch um die Erstellung einer Sandbox gehen mit der wir JavaScript von User-Input oder genierten Applikations-Code entsprechend in einer Isolation ausführen können. Das soll verhindern, dass der Sandbox-Code außerhalb der ihm gegebenen Parameter Funktionen ausführen, oder Variablen modifizieren kann.

Warum das Ganze?

JavaScript an sich eignet sich sehr gut um eingebettet zu laufen. Mit der V8-Engine gibt es hierbei eine sehr gute Implementierung von Google. Man kann so JavaScript zum Beispiel in Verbindung mit Java-Bindings nutzen und so Nutzer ziemlich leicht Java-Code oder Prozesse zu scripten, ohne das Core-System anzupassen. Doch warum sollte sich das Ganze auf die Welt außerhalb von JavaScript beschränken?

Der eine oder andere fragt sich an der Stelle vielleicht: Das gibt es doch schon? - Und ja, das gibt es schon doch die meisten Projekte setzen hierbei auf eine komplette Neuimplementierung von JavaScript. Und im Browser haben wir ja schon einen leistungsfähigen JavaScript-Interpreter wie z. B. die V8-Engine in Chrome.

Die Idee ist also die bestehenden Funktionalitäten des Browser zu erweitern und so eine leichtgewichtige Sandbox-Lösung zu schaffen.

Und natürlich der wichtigste Grund ...

Die Voraussetzungen

Die erste und ziemlich offensichtliche Voraussetzung: Wir benötigen eine JavaScript-Runtime mit DOM-API, Node.js, sofern nicht in Kombination mit Electron o. ä. scheidet also hier ganz klar aus.

Um nicht mit veralteten Hacks arbeiten zu müssen sollte der entsprechende Browser oder die Runtime mindestens auf Stand von ES2016 unterstützt werden.

Umsetzung

Für die Umsetzung gibt es folgenden Plan: Ich zweckentfremde einen iFrame um darauf eine Funktion zu erstellen, welche den auszuführenden Code kapselt. Nachdem diese Funktion mit dem Kontext des iFrame instanziert wurde, wird das iFrame entsprechend aus dem DOM entfernt und derefenziert. Damit ist der entsprechende Kontext nicht mehr vorhanden, weshalb alerts und sonstige Funktionen auf dem Document sowie Window nicht mehr ausgeführt werden können. Der enstprechende Kontext ist damit nicht das aktuelle Dokument, was auch verhindert das entsprechend das DOM modifiziert, Dialoge oder ähnliches angezeigt werden können.

Sandbox-Skript

Die Sandbox als simple Funktion sieht in der minimalen Ausprägungsstufe dann so aus:

createSandboxedStatements - Die Implementierung der Sandbox
Beispielhafter Aufruf

Also was genau passiert hier?

  1. iFrame erstellen
  2. iFrame modifizieren um Skriptausführung und Cross-Calls zuzulassen
  3. Function erstellen mit den Statements
  4. iFrame vom DOM entfernen und zerstören
  5. Das Function-Objekt zurückgeben als Sandbox

Performance

Dadurch, dass die Function, die erstellt wird eine First-Class-Function ist und regulär optimiert und auch interpretiert wird, haben wir an dieser Stelle die selbe Performance wie bei einer "nativen" Funktion.

Das Ganze kannst du gerne selbst testen,  ich empfehle an dieser Stelle die eigens für so etwas implementierte Performance-API.

Sicherheit

Diese Implementierung ist an sich schon mal abgesichert gegen Modifizierung des aktuellen DOM, oder Aufruf sowie Modifizierung von Variablen. Browser-integrierte Funktionen auf Window und Document sind unterbunden, dadurch dass das entsprechende iFrame gar nicht mehr vorhanden ist.

An dieser Stelle gibt es jedoch keine eingebaute Garantie das z. B. über HTML-Elemente die als Parameter der Sandbox übergeben werden über diverse Tricks auf das DOM zugreifen kann bzw. auf andere Elemente. An dieser Stelle muss man sich im Klaren sein wie vertrauenswürdig der auszuführende Code ist.

An dieser Stelle jedoch auch eine generelle, wenn auch offensichtliche Warnung: Trotz der Sandbox ist die Ausführung von Fremdcode nie unproblematisch. Da wir hier natürlich auch unzähligen potenziellen Lücken ausgesetzt sind

Error-Handling

Syntax oder Laufzeitfehler können mit einem simplen try/catch um die Sandbox abgehandelt werden. Nachdem der Kontext entsprechend innerhalb dieses Blockes stattfindet, lassen sich so sowohl Syntax als auch Laufzeitfehler gleichermaßen abfangen und behandeln.

Hierzu findet sich auch noch mal ein Beispiel auf GitHub.

Summary

Mit relativ wenig Code wurde so eine ausreichend sichere Sandbox-Lösung für die meisten Einsatzzwecke generiert. Dabei wurde der Bau eines eigenen Interpreter verhindert und so unnötige Arbeit und Redundanz vermieden. Für alle Interessierten habe ich noch ein sehr primitives Demo-Projekt auf GitHub.