1 Punkte von GN⁺ 2024-07-08 | 1 Kommentare | Auf WhatsApp teilen
  • @sebastianwessel/quickjs ist 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-basiertes expect
  • In den Nutzungsbeispielen wird QuickJS wasm einmal mit loadQuickJs() geladen; anschließend wird Code mit runSandboxed() und evalCode() ausgeführt, wobei Optionen wie allowFetch, allowFs und env ü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/quickjs ist 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 variant aus @jitl/quickjs-ng-wasmfile-release-sync importiert; aus @sebastianwessel/quickjs werden loadQuickJs und SandboxOptions verwendet
  • loadQuickJs(variant) lädt und initialisiert QuickJS wasm; da dieser Vorgang ressourcenintensiv ist, wird empfohlen, ihn nach Möglichkeit nur einmal auszuführen
  • loadQuickJs() gibt runSandboxed zurück, das anschließend für die Ausführung in der Sandbox verwendet wird
  • Ein Beispiel für SandboxOptions umfasst folgende Optionen
    • allowFetch: true: injiziert fetch in den Code und erlaubt Datenanfragen
    • allowFs: true: mountet ein virtuelles Dateisystem und stellt das Modul node:fs bereit
    • env: übergibt Werte für Umgebungsvariablen, die im Sandbox-Code verwendet werden

Mögliche Aktionen im Ausführungsbeispiel

  • Der Code in der Sandbox importiert join aus path und ruft join('src','dist') auf, wodurch src/dist im Log des Host-Systems ausgegeben wird
  • Er liest env.MY_ENV_VAR und gibt "env var value" im Log des Host-Systems aus
  • Er erstellt mit new URL('https://example.com') eine URL, ruft fetch(url) auf und gibt anschließend das Ergebnis von f.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

 
GN⁺ 2024-07-08
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, fetch mit denselben Cookies wie die fetch-Funktion des Hosts aufzurufen. Man sollte fetch nicht 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

    • Ich frage mich, ob man mit einem iframe das Fetch-Cookie-Problem verhindern kann, unabhängig davon, ob diese Bibliothek verwendet wird. Oder ob es auch in einem iframe weiterhin ein Problem bleibt.
  • 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.

    • Salesforce löst das mit einer Kombination aus Web Components und einem gepatchten ShadowRoot. Code, der eine Referenz auf die Shadow Root hat, kann nicht in den Rest des Dokuments hinauslaufen, und mit einer sicheren Evaluierungsfunktion im Zusammenhang mit SES (Secure EcmaScript) werden die globalen Objekte eingeschränkt, auf die nicht vertrauenswürdige Skripte zugreifen können.
      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
    • Am nächsten kommt Allen Wirfs-Brocks jsmirrors-Prototyp, aber meines Wissens ist er für das DOM nie bis zur Spezifikation gelangt, und das war wohl von Anfang an auch nicht wirklich beabsichtigt. Er behandelte nur Capabilities für das Programmiersystem JavaScript.
      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.createElement DOM-Nodes erzeugen lassen. Tatsächlich will man ihr aber nicht Zugriff auf document.createElement geben, sondern eine add-button-Capability. Also implementiert man addButton.
      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.
    • Wenn der Zugriff wirklich sicher sein soll, bleibt nur, dem Drittanbieter-Code ein iframe auf einer anderen Domain zu geben und das sandbox-Attribut zu setzen.
      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...
    • Ein möglicher Ansatz wäre wohl, eine API hinzuzufügen, über die Drittanbieter das HTML, das sie rendern möchten, als String schicken, diesen mit einer der vielen Libraries zu parsen und dann auf Basis der geparsten Daten das DOM neu aufzubauen.
      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.
    • Aus dem Stegreif könnte man es etwa so versuchen: Das Backend fordert den Drittanbieter-Code an und verknüpft diesen Code mit einer Hash-Sequenz.
      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 ein script-Tag eingefügt wird, das den Drittanbieter-Code von der eigenen Domain anfordert, und fügt zur Nachverfolgung den Hash-Wert als data-Attribut dieses script-Tags hinzu.

Im Backend den Third-Party-Code so anpassen, dass alle Instanzen von document. und window. durch document.getElementById(hash_value). ersetzt werden und alle Query-Selektoren mit #hash_value beginnen
.parentNode im Element-Prototyp durch eine eigene Property ersetzen, um Versuche zu prüfen und zu blockieren, den bereitgestellten Container zu verlassen
Danach 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 eval deutlich 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 createRuntime außer fetch auch 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 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.

    • Wenn die Startzeit des Node.js-Prozesses gegenüber der Laufzeit des Skripts dominiert, kann es Performance-Gewinne geben. Ich vermute, dass das auf ziemlich viele Skripte im Stil von Serverless Functions zutrifft.
  • 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.

    • Ich weiß nicht, wie man das mit QJS macht, aber wenn du einen Bundler im Browser ausführen willst, scheinen WebContainers genau dafür gemacht zu sein.
      [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.

    • Ich würde besonders in Kombination mit Service Workern gern mehr darüber wissen.
      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.
    • iframes haben zu weitreichende Berechtigungen. Zum Beispiel kann ein iframe Daten an Dritte exfiltrieren.