HN vorgestellt: JavaScript in einer WebAssembly-QuickJS-Sandbox ausführen
(github.com/sebastianwessel)@sebastianwessel/quickjsist ein TypeScript-Paket, das JavaScript- und TypeScript-Code sicher in einer Sandbox ausführt, in der die QuickJS-Engine nach WebAssembly kompiliert wurde- Der Fokus liegt darauf, nicht vertrauenswürdigen Code isoliert auszuführen; über die QuickJS-WebAssembly-Sandbox stellt es eine schlanke und schnelle Ausführungsumgebung bereit
- Unterstützt grundlegende Node.js-Module, das Mounten eines virtuellen Dateisystems, das Mounten benutzerdefinierter Node-Module, stellt einen
fetch-Client bereit und enthält einen Test-Runner sowie ein Chai-basiertesexpect - In den Nutzungsbeispielen wird QuickJS wasm einmal mit
loadQuickJs()geladen; anschließend wird Code mitrunSandboxed()undevalCode()ausgeführt, wobei Optionen wieallowFetch,allowFsundenvübergeben werden - Entwickler, die in TypeScript-Anwendungen externen JavaScript-Code ausführen müssen, können es als Ausführungsumgebung nutzen, die Isolation, Performance und eine einfache API kombiniert
QuickJS-WebAssembly-Sandbox-Paket
@sebastianwessel/quickjsist ein TypeScript-Paket, mit dem JavaScript- und TypeScript-Code in einer WebAssembly-Sandbox ausgeführt werden kann- Es verwendet die nach WebAssembly kompilierte QuickJS-Engine und ist darauf ausgelegt, nicht vertrauenswürdigen Code in einer isolierten Umgebung auszuführen
- Das Paket nutzt die schlanke und schnelle QuickJS-Engine, um eine robuste Umgebung für die Codeausführung bereitzustellen
- Es bietet vollständige Dokumentation, Beispiele und einen Online-Playground
Funktionen
- Sichere Ausführung: Führt nicht vertrauenswürdigen JavaScript- und TypeScript-Code sicher in einer isolierten Umgebung aus
- Grundlegende Node.js-Module: Bietet Unterstützung für grundlegende Standard-Node.js-Module für typische Anwendungsfälle
- Dateisystem: Ein virtuelles Dateisystem kann gemountet werden
- Benutzerdefinierte Node-Module: Benutzerdefinierte Node-Module können gemountet werden
- Netzwerkaufrufe: Stellt einen
fetch-Client bereit, um http(s)-Aufrufe auszuführen - Test-Unterstützung: Enthält einen Test-Runner und ein Chai-basiertes
expect - Performance und Integrierbarkeit: Nutzt die Leichtgewichtigkeit und Effizienz der QuickJS-Engine und lässt sich einfach in bestehende TypeScript-Projekte integrieren
- Einfache API: Bietet eine benutzerfreundliche API zum Ausführen und Verwalten von JavaScript- und TypeScript-Code in der Sandbox
Grundlegender Ablauf der Nutzung
- Zuerst wird
variantaus@jitl/quickjs-ng-wasmfile-release-syncimportiert; aus@sebastianwessel/quickjswerdenloadQuickJsundSandboxOptionsverwendet loadQuickJs(variant)lädt und initialisiert QuickJS wasm; da dieser Vorgang ressourcenintensiv ist, wird empfohlen, ihn nach Möglichkeit nur einmal auszuführenloadQuickJs()gibtrunSandboxedzurück, das anschließend für die Ausführung in der Sandbox verwendet wird- Ein Beispiel für
SandboxOptionsumfasst folgende OptionenallowFetch: true: injiziertfetchin den Code und erlaubt DatenanfragenallowFs: true: mountet ein virtuelles Dateisystem und stellt das Modulnode:fsbereitenv: übergibt Werte für Umgebungsvariablen, die im Sandbox-Code verwendet werden
Mögliche Aktionen im Ausführungsbeispiel
- Der Code in der Sandbox importiert
joinauspathund ruftjoin('src','dist')auf, wodurchsrc/distim Log des Host-Systems ausgegeben wird - Er liest
env.MY_ENV_VARund gibt"env var value"im Log des Host-Systems aus - Er erstellt mit
new URL('https://example.com')eine URL, ruftfetch(url)auf und gibt anschließend das Ergebnis vonf.text()zurück - Code wird in der Form
runSandboxed(async ({ evalCode }) => evalCode(code), options)ausgeführt - Ein Beispielergebnis hat die Form
{ ok: true, data: '<!doctype html>\n<html>\n[....]</html>\n' }
Lizenz und geeignete Einsatzbereiche
- Dieses Projekt wird unter der MIT License bereitgestellt
- Es eignet sich für Entwickler, die JavaScript-Code sicher innerhalb von TypeScript-Anwendungen ausführen möchten
- Über die QuickJS-WebAssembly-Sandbox bietet es eine Ausführungsumgebung, die Performance und Sicherheit verbindet
1 Kommentare
Hacker-News-Kommentare
Als Autor der Laufzeitbibliothek quickjs-emscripten gefällt mir der ergonomische Stil einer Standardbibliothek, der hier für quickjs-emscripten entwickelt wurde.
Ich frage mich, ob das schon im Browser oder mit einem Bundler ausprobiert wurde. Der Ansatz, den Variantennamen als String zu nehmen und dynamisch an
import(variantName)zu übergeben, könnte mit Tools wie Webpack nicht gut zusammenspielen.Sicherheitswarnung: Diese Bibliothek erlaubt es nicht vertrauenswürdigem Guest-Code,
fetchmit denselben Cookies wie diefetch-Funktion des Hosts aufzurufen. Man solltefetchnicht aktivieren und dann nicht vertrauenswürdigen Code ausführen.Wenn Code innerhalb der Sandbox beliebige HTTP-APIs aufrufen kann, authentifiziert als Host-Kontext, dann ist das keine „Sandbox“.
Der Grund, warum quickjs-emscripten Low-Level ist und magische Funktionen vermeidet, ist, dass man mit Sicherheit sagen können möchte, dass die bereitgestellten APIs sicher sind. Anfragen nach magischer Serialisierung oder einfachem Netzwerk-/Dateisystemzugriff werden größtenteils abgelehnt, weil solcher Code ein Bereich ist, in dem leicht Sicherheitsfehler entstehen.
Wenn man nicht vertrauenswürdigen Code ausführt, muss man nicht nur die Sandbox selbst sorgfältig auditieren, sondern auch den Code, den man für die APIs geschrieben hat, die man der Sandbox bereitstellt.
Ich bin durch einen Kommentar eines anderen HN-Nutzers, der nach Fetch-Cookies fragte, auf das potenzielle Sicherheitsproblem aufmerksam geworden.
Weiterführende Lektüre: Figma-Artikel zur Sicherheit der Plugin-Sandbox https://www.figma.com/blog/how-we-built-the-figma-plugin-sys..., https://www.figma.com/blog/an-update-on-plugin-security/, Quickjs-emscripten-README https://github.com/justjake/quickjs-emscripten
Es gibt viele Möglichkeiten, JavaScript sowohl serverseitig als auch im Browser zu sandboxen, aber ich weiß nicht, ob es eine Möglichkeit gibt, DOM-Zugriff zu „sandboxen“.
Zum Beispiel so, dass man einem nicht vertrauenswürdigen Drittanbieter nur Zugriff auf DOM-Elemente an vorab festgelegten Stellen gibt. Soweit ich weiß, ist die einzige Technik, die das erlaubt, ein iframe, aber leider ist das schwergewichtig und langsam.
Ich baue eine App, die Plugins hosten kann, und wenn man Plugins DOM-Zugriff gibt, scheint das buchstäblich alles zu ermöglichen.
Diese sichere Evaluierungsfunktion ist ziemlich beeindruckend. Der Kern scheint vermutlich hier zu liegen: https://github.com/Agoric/realms-shim/blob/v1.1.0/src/evalua...
Es gibt auch die Idee isolierter Web Components, um dieses Problem auf Plattformebene zu lösen: https://github.com/WICG/webcomponents/issues/1002
Inspiriert von jsmirrors könnte man selbst so etwas wie „dommirrors“ versuchen, aber das wäre ein großes Stück Arbeit. Es gibt auch Umwege, mit denen man jsmirrors unverändert nutzen und einen Teil des Gewünschten erreichen kann, aber besonders angenehm zu verwenden ist das nicht.
Allerdings ist es fast sicher nicht das, was man wirklich will, DOM-Zugriff zu vermitteln oder zu simulieren. Besser ist, herauszufinden, was man der anderen Seite tatsächlich erlauben möchte, und dann nur die Capability zu geben, die dieses Verhalten ermöglicht.
Wenn man ihr zum Beispiel erlauben möchte, irgendwo einen Button hinzuzufügen, könnte man denken, man müsse ihr ein Elternelement als Ankerpunkt geben und sie mit
document.createElementDOM-Nodes erzeugen lassen. Tatsächlich will man ihr aber nicht Zugriff aufdocument.createElementgeben, sondern eine add-button-Capability. Also implementiert manaddButton.Mehr dazu: <https://news.ycombinator.com/item?id=30703531#30706060>
Auf Leute, die sagen, CSP sei für diesen Zweck gedacht, sollte man besser nicht hören. Ist es nicht. Genau genommen halte ich CSP selbst für den Zweck, für den es ursprünglich gedacht war, für schlecht designten, nutzerfeindlichen Müll, der nie so hätte implementiert und erweitert werden sollen, wie es passiert ist. Sich darauf zu verlassen, ist riskant.
Auf der Parent-Seite kann man die Größe des DOM-Inhalts messen und die iframe-Größe innerhalb bestimmter Grenzen anpassen, sodass es sich natürlicher in die App-UI integrieren lässt.
Je nach Sichtbarkeit zwischen Nutzern und Vertrauensniveau muss man im iframe auf Impersonation-/Phishing- und Clickjacking-Versuche achten. Idealerweise sollte der Frame so gesperrt sein, dass er überhaupt keine Web-Requests stellen kann; das bedeutet auch, dass keine Bilder geladen werden können.
Dann gäbe es, selbst wenn ein Nutzer dazu verleitet wird, sein Passwort in ein gefälschtes Passwortformular einzugeben, keine Möglichkeit, die Daten aus dem Frame herauszuschleusen.
Die wichtigste Methode, um einzuschränken, welche Arten von Ressourcen ein iframe anfordern kann, ist Content-Security-Policy; damit lassen sich Drittanbieter-Bilder, Skripte usw. komplett abschalten.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameE...
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
Außerdem sollte man weitere sandbox-Attribute aktivieren und den Zugriff auf privacy-sensitive DOM-APIs wie die Webcam deaktivieren.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/if...
https://developer.mozilla.org/en-US/docs/Web/Security/IFrame...
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pe...
So kann man die erlaubten HTML-Elemente und Attribute per Whitelist beschränken. Wenn man zusätzlich native DOM-Event-Listener erlauben will, wird es komplizierter, aber nicht unmöglich.
Man bräuchte etwas wie eine API, die den Event-Namen als String und die ID des Elements entgegennimmt, das das Event empfangen soll; dann müsste man das entsprechende Event im echten DOM erkennen und dasselbe Event in die verwendete JavaScript-Sandbox replizieren. Innerhalb dieser Sandbox müsste der zuvor genannte API-Zugriff verfügbar sein.
Das Backend ändert das HTML dynamisch und fügt ein
div-Tag ein, dessen id-Wert die Hash-Sequenz ist. Außerdem ändert es das HTML so, dass einscript-Tag eingefügt wird, das den Drittanbieter-Code von der eigenen Domain anfordert, und fügt zur Nachverfolgung den Hash-Wert als data-Attribut diesesscript-Tags hinzu.Im Backend den Third-Party-Code so anpassen, dass alle Instanzen von
document.undwindow.durchdocument.getElementById(hash_value).ersetzt werden und alle Query-Selektoren mit#hash_valuebeginnen.parentNodeimElement-Prototyp durch eine eigene Property ersetzen, um Versuche zu prüfen und zu blockieren, den bereitgestellten Container zu verlassenDanach das HTML-Dokument an den Browser senden. Es ist in Ordnung, wenn der Third-Party-Code kaputtgeht. Die Einschränkungen müssen den Drittanbietern mitgeteilt werden, und sie sollten ihren Code selbst testen, bevor sie ihn an den Server schicken
Wichtig ist nur, dass der Code nicht aus dem dynamisch bereitgestellten Container ausbrechen kann. Dieser Teil sollte regelmäßig getestet werden, um Sicherheitsverstöße zu finden
Vielleicht funktioniert es in der Praxis nicht, aber es wäre interessant, damit zu experimentieren
Zufällig habe ich letzte Woche QuickJS ausprobiert und bin am Ende bei isolated-vm gelandet. Beide erfüllten die Sicherheitsanforderungen, aber isolated-vm war beim Setup, beim Teardown und beim Overhead für die Ausführung von
evaldeutlich performanter.Interessanter Ansatz. Als Autor einer anderen JavaScript-Sandbox-Bibliothek[1], die über Worker isoliert und Techniken zum Bereinigen der JavaScript-Umgebung nutzt, sehe ich die Interpretation von JavaScript – also JavaScript in JavaScript oder, wie in diesem Fall, JavaScript in WASM – als die Variante, die den höchsten Grad an Isolation bietet.
Außerdem ist man nicht direkt Bugs in der Host-JavaScript-VM selbst ausgesetzt. Wenn man auf Node abzielt, kann das noch wichtiger sein, weil Node.js, abgesehen von einigen neueren Entwicklungen, im Unterschied zu Deno offenbar nicht mit Isolation und Sandboxing im Sinn entworfen wurde.
Beim Blick auf die API sehe ich nicht, ob
createRuntimeaußerfetchauch Aufrufe in die Host-Umgebung definieren kann. Diese Funktion wäre ziemlich nützlich, weil man die Kommunikation mit der Außenwelt kontrolliert einschränken könnte, statt sie komplett zu erlauben oder komplett zu blockieren.Ebenso scheint auch der Browser nicht unterstützt zu werden. Zumindest war das bei einem kurzen Check mit esm.sh so. Auch das könnte ein nützliches Feature sein.
Ich werde es testen, weil mich interessiert, wie groß der Overhead ist; wie gesagt, der Ansatz selbst wirkt ziemlich solide.
[1] @exact-realty/lot
Es gibt außerdem APIs zum Exponieren von Host-Funktionen, zum Aufrufen von Gast-Funktionen, für Custom Module Loader usw.: https://github.com/justjake/quickjs-emscripten?tab=readme-ov...
Dokumentation zur API
newFunction: https://github.com/justjake/quickjs-emscripten/blob/main/doc...Es wirkt so, als wären CPUs inzwischen so schnell geworden, dass wir jetzt Interpreter in Interpretern laufen lassen sollen.
Performance würde ich nicht als Vorteil nennen, wenn man JavaScript in QuickJS ausführt. QuickJS kann mit der Host-JavaScript-VM überhaupt nicht konkurrieren.
Es kann aber schneller sein als ein alter C-Interpreter oder ein in JavaScript implementierter Interpreter.
Damit ließe sich von Nutzern bereitgestellter JavaScript-Code ausführen. Ich war auf der Suche nach einer Möglichkeit, TypeScript-Code von Nutzern in einer Sandbox-Umgebung mit einem Bundler zu bündeln.
Ich frage mich, ob es Empfehlungen gibt, wie man einen Bundler wie webpack in QuickJS ausführt.
[1]: https://webcontainers.io/
Sehr cool. Wenn es nach WASM kompiliert wurde, frage ich mich, ob es im Browser ausgeführt werden kann. Falls das möglich ist und man
fetch-Requests ohne angehängte Cookies absetzen kann, wäre das interessant.In meinem früheren Job hatten wir bei Quickjs-emscripten so viele Segmentation Faults und stille Fehler, dass das Projekt auf Eis gelegt wurde.
Wenn ich es noch einmal machen müsste, würde ich wohl eine Engine verwenden, die bei mehr Programmen korrekt funktioniert und ein offiziell freigegebenes WASM-Bundle hat.
Ich dachte, man könne Code mit einem iframe sicher sandboxen, bin mir aber nicht sicher. Natürlich ließen sich mit einem eigenen Interpreter wohl mehr Funktionen wie engere Zeitlimits und Custom APIs einbauen.
Derzeit verwende ich ein iframe, das Code per window message entgegennimmt, den eingegebenen Code auswertet und die Antwort per window message zurückgeben kann; das funktioniert ziemlich gut. Ich bin mir allerdings nicht sicher, ob es Schlupflöcher gibt, um aus der Sandbox auszubrechen.
Für komplexere Fälle, etwa Paketabhängigkeiten, wollte ich sie mit Babel parsen und dann eine Import Map erstellen, die ein CDN (esm.sh) nutzt; aber die über das CDN bereitgestellten Versionen funktionierten teils nicht richtig.
Deshalb habe ich StackBlitz verwendet, und das funktioniert ziemlich gut, hat aber in Umgebungen ohne sicheren Kontext ein paar Probleme.
Am Ende habe ich einen kleinen Webserver gebaut, der Code und Abhängigkeiten (package.json) entgegennimmt, in einem Docker-Container einen Vite-Build ausführt und das Ergebnis zurückgibt. Das funktioniert einigermaßen ordentlich, ist aber gelegentlich langsam.
Es wäre schön, vollständig clientseitig bauen zu können, und StackBlitz macht ungefähr so etwas.