1 points par GN⁺ 2024-07-08 | 1 commentaires | Partager sur WhatsApp
  • @sebastianwessel/quickjs est un package TypeScript qui permet d’exécuter en toute sécurité du code JavaScript et TypeScript dans un bac à sable où le moteur QuickJS est compilé en WebAssembly
  • Il se concentre sur l’exécution isolée de code non fiable et fournit, via le bac à sable QuickJS WebAssembly, un environnement d’exécution léger et rapide
  • Il inclut la prise en charge de modules Node.js de base, le montage d’un système de fichiers virtuel, le montage de modules Node personnalisés, un client fetch, un runner de tests et un expect basé sur Chai
  • Les exemples d’utilisation consistent à charger une fois le wasm QuickJS avec loadQuickJs(), puis à exécuter le code avec runSandboxed() et evalCode(), en passant des options comme allowFetch, allowFs et env
  • Les développeurs qui doivent exécuter du code JavaScript externe dans une application TypeScript peuvent l’utiliser comme environnement d’exécution conciliant isolation, performances et simplicité d’API

Package de bac à sable QuickJS WebAssembly

  • @sebastianwessel/quickjs est un package TypeScript permettant d’exécuter du code JavaScript et TypeScript dans un bac à sable WebAssembly
  • Il utilise le moteur QuickJS compilé en WebAssembly et est conçu pour exécuter du code non fiable dans un environnement isolé
  • Le package s’appuie sur le moteur QuickJS, léger et rapide, afin de fournir un environnement robuste pour l’exécution de code
  • Une documentation complète, des exemples et un playground en ligne sont disponibles

Fonctionnalités proposées

  • Exécution sécurisée : exécute du code JavaScript et TypeScript non fiable dans un environnement sûr et isolé
  • Modules Node.js de base : fournit la prise en charge de modules Node.js standards de base pour les cas d’usage courants
  • Système de fichiers : permet de monter un système de fichiers virtuel
  • Modules Node personnalisés : permet de monter des modules Node personnalisés
  • Appels réseau : fournit un client fetch pour effectuer des appels http(s)
  • Prise en charge des tests : inclut un runner de tests et un expect basé sur Chai
  • Performances et intégration : tire parti de la légèreté et de l’efficacité du moteur QuickJS, et s’intègre facilement aux projets TypeScript existants
  • API simple : fournit une API conviviale pour exécuter et gérer du code JavaScript et TypeScript dans le bac à sable

Flux d’utilisation de base

  • Il faut d’abord importer variant depuis @jitl/quickjs-ng-wasmfile-release-sync, puis utiliser loadQuickJs et SandboxOptions depuis @sebastianwessel/quickjs
  • loadQuickJs(variant) charge et initialise le wasm QuickJS ; cette opération étant gourmande en ressources, il est recommandé, si possible, de ne l’effectuer qu’une seule fois
  • loadQuickJs() renvoie runSandboxed, qui est ensuite utilisé pour l’exécution dans le bac à sable
  • Un exemple de SandboxOptions inclut les options suivantes
    • allowFetch: true : injecte fetch dans le code et autorise les requêtes de données
    • allowFs: true : monte un système de fichiers virtuel et fournit le module node:fs
    • env : transmet les valeurs de variables d’environnement à utiliser par le code exécuté dans le bac à sable

Comportements possibles dans l’exemple d’exécution

  • Le code dans le bac à sable importe join depuis path et appelle join('src','dist'), ce qui affiche src/dist dans les logs du système hôte
  • Il lit env.MY_ENV_VAR et affiche "env var value" dans les logs du système hôte
  • Il crée une URL avec new URL('https://example.com'), appelle fetch(url), puis renvoie le résultat de f.text()
  • Le code est exécuté sous la forme runSandboxed(async ({ evalCode }) => evalCode(code), options)
  • Un exemple de résultat est de la forme { ok: true, data: '<!doctype html>\n<html>\n[....]</html>\n' }

Licence et cas d’usage adaptés

  • Ce projet est distribué sous MIT License
  • Il convient aux développeurs souhaitant exécuter du code JavaScript en toute sécurité au sein d’applications TypeScript
  • Le bac à sable QuickJS WebAssembly fournit un environnement d’exécution combinant performances et sécurité

1 commentaires

 
GN⁺ 2024-07-08
Commentaires sur Hacker News
  • En tant qu’auteur de la bibliothèque d’exécution quickjs-emscripten, j’apprécie le style de bibliothèque standard ergonomique créé pour quickjs-emscripten
    Je me demande si cela a été testé dans un navigateur ou avec un bundler. Le fait de recevoir le nom de la variante sous forme de chaîne et de le passer dynamiquement à import(variantName) risque de mal s’accorder avec des outils comme Webpack
    Avertissement de sécurité : cette bibliothèque permet à du code invité non fiable d’appeler fetch avec les mêmes cookies que la fonction fetch de l’hôte. Il ne faut pas activer fetch et exécuter du code non fiable
    Si du code à l’intérieur du sandbox peut appeler des API HTTP arbitraires authentifiées dans le contexte hôte, alors ce n’est pas un « sandbox »
    Si quickjs-emscripten est bas niveau et évite les fonctionnalités magiques, c’est pour pouvoir affirmer avec assurance que l’API fournie est sûre. Les demandes de sérialisation magique ou d’accès facile au réseau/système de fichiers sont généralement refusées, car ce type de code est une zone propice aux erreurs de sécurité
    Quand on exécute du code non fiable, il faut auditer soigneusement non seulement le sandbox lui-même, mais aussi le code écrit pour les API exposées au sandbox
    C’est en voyant le commentaire d’un autre utilisateur HN poser une question sur les cookies de Fetch que j’ai repéré ce problème de sécurité potentiel
    Pour aller plus loin : article sur la sécurité du sandbox des plugins Figma https://www.figma.com/blog/how-we-built-the-figma-plugin-sys..., https://www.figma.com/blog/an-update-on-plugin-security/, README de quickjs-emscripten https://github.com/justjake/quickjs-emscripten

    • Je me demande si l’utilisation d’un iframe permettrait d’empêcher le problème des cookies de fetch, indépendamment de l’usage de cette bibliothèque, ou si cela resterait malgré tout un problème avec un iframe
  • Il existe de nombreuses façons de sandboxer JavaScript côté serveur comme côté navigateur, mais je ne sais pas s’il existe un moyen de « sandboxer » l’accès au DOM
    Par exemple, donner à un tiers non fiable l’accès uniquement à des éléments du DOM situés à des emplacements prédéfinis. À ma connaissance, la seule technologie qui permette cela est l’iframe, mais malheureusement c’est lourd et lent
    Je suis en train de créer une application capable d’héberger des plugins, et si on donne aux plugins l’accès au DOM, on a l’impression qu’ils peuvent littéralement tout faire

    • Salesforce gère cela en combinant des composants web et un ShadowRoot patché. L’idée est d’empêcher du code disposant d’une référence vers une racine shadow de s’échapper vers le reste du document, et de restreindre les objets globaux accessibles aux scripts non fiables via des fonctions d’évaluation sûres liées à SES (Secure EcmaScript)
      Ces fonctions d’évaluation sûres sont assez impressionnantes. Le cœur du mécanisme est probablement ici : https://github.com/Agoric/realms-shim/blob/v1.1.0/src/evalua...
      Il existe aussi l’idée de composants web isolés pour résoudre ce problème au niveau de la plateforme : https://github.com/WICG/webcomponents/issues/1002
    • Ce qui s’en approche le plus est probablement le prototype jsmirrors d’Allen Wirfs-Brock, mais à ma connaissance il n’est jamais allé jusqu’à une spécification pour le DOM, et ce n’était pas vraiment son objectif au départ. Il ne traitait que des capacités (capability) pour JavaScript en tant que système de programmation
      On pourrait essayer soi-même quelque chose comme « dommirrors » inspiré de jsmirrors, mais c’est un gros chantier. Il existe aussi des solutions de contournement permettant d’atteindre en partie l’objectif avec jsmirrors tel quel, mais ce n’est pas très pratique à utiliser
      Cela dit, fournir un accès au DOM en le médiatisant ou en le simulant n’est presque certainement pas ce qu’on veut réellement. Il vaut mieux déterminer précisément ce qu’on souhaite autoriser, puis ne donner que les capacités nécessaires à ces actions
      Par exemple, si l’on veut permettre à quelqu’un d’ajouter un bouton quelque part, on peut penser qu’il faut lui fournir un point d’ancrage sous forme d’élément parent et lui laisser créer des nœuds DOM avec document.createElement. Mais en réalité, ce qu’on veut n’est pas lui donner l’accès à document.createElement, c’est lui donner la capacité add-button. Il suffit donc d’implémenter addButton
      Pour en savoir plus : <https://news.ycombinator.com/item?id=30703531#30706060>
      Mieux vaut ne pas écouter ceux qui disent que le CSP est fait pour cela. Ce n’est pas le cas. Pour être précis, même dans l’usage qu’il visait à l’origine, le CSP est selon moi une conception médiocre, hostile aux utilisateurs, qui n’aurait jamais dû être implémentée ni étendue sous sa forme actuelle. S’y fier est risqué
    • Pour un accès vraiment sûr, la seule méthode consiste à donner au code tiers une iframe sur un autre domaine et à définir l’attribut sandbox
      La page parente peut mesurer la taille du contenu DOM et ajuster la taille de l’iframe dans certaines limites, ce qui permet une intégration plus naturelle dans l’interface de l’application
      Selon le niveau d’exposition entre utilisateurs et le niveau de confiance, il faut se méfier des tentatives d’usurpation/phishing et de clickjacking à l’intérieur de l’iframe. Idéalement, il faudrait verrouiller le cadre pour qu’il ne puisse faire aucune requête web, ce qui signifie aussi pas de chargement d’images
      Ainsi, même si l’utilisateur est piégé et saisit son mot de passe dans un faux formulaire, le cadre n’a aucun moyen d’exfiltrer les données hors de l’iframe
      Le principal moyen de limiter les types de ressources qu’une iframe peut demander est la Content-Security-Policy, qui permet de désactiver les images tierces, les scripts, etc.
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameE...
      https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
      Il faut aussi activer d’autres attributs sandbox et désactiver l’accès aux API DOM sensibles pour la vie privée, comme la webcam
      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...
    • Une autre possibilité serait d’ajouter une API qui reçoit sous forme de chaîne le HTML que le tiers veut rendre, puis de le parser avec l’une des nombreuses bibliothèques disponibles, avant de reconstruire le DOM à partir des données parsées
      Cela permet de limiter par liste blanche les éléments HTML et les attributs autorisés. Si l’on veut autoriser aussi l’écoute d’événements DOM natifs, cela devient plus complexe, mais ce n’est pas impossible
      Il faudrait une API qui reçoive par exemple le nom de l’événement sous forme de chaîne et l’id de l’élément devant recevoir cet événement, puis détecter cet événement dans le DOM réel et en recopier l’équivalent dans le sandbox JavaScript utilisé. À l’intérieur de ce sandbox, il faudrait bien sûr avoir accès à l’API mentionnée plus haut
    • À chaud, on pourrait imaginer quelque chose comme ceci : récupérer le code tiers côté backend et l’associer à une séquence de hachage
      Le backend modifie alors dynamiquement le HTML pour y insérer une balise div dont la valeur de id est cette séquence de hachage. Il modifie aussi le HTML pour insérer une balise script qui récupère le code tiers depuis son propre domaine, et ajoute la valeur de hachage dans un attribut data de cette balise script à des fins de suivi

Côté backend, modifier le code tiers pour remplacer toutes les occurrences de document. et window. par document.getElementById(hash_value)., et faire en sorte que tous les sélecteurs de requête commencent par #hash_value
Remplacer .parentNode du prototype Element par une propriété personnalisée, afin de pouvoir vérifier et bloquer les tentatives de sortie du conteneur fourni
Envoyer ensuite le document HTML au navigateur. Ce n’est pas grave si le code tiers casse. Il faut simplement informer le tiers des contraintes, et c’est à lui de tester son propre code avant de l’envoyer au serveur
L’important est seulement d’empêcher le code de sortir dynamiquement du conteneur fourni. Cette partie doit être testée régulièrement pour détecter des failles de sécurité
Il est possible que cela ne fonctionne pas réellement, mais ce serait amusant à expérimenter

  • J’ai testé QuickJS par hasard la semaine dernière, et j’ai finalement adopté isolated-vm. Les deux répondaient aux exigences de sécurité, mais isolated-vm était bien plus performant en termes de configuration, de démontage et de surcharge d’exécution de eval

  • Approche intéressante. En tant qu’auteur d’une autre bibliothèque de sandbox JavaScript[1] qui isole via des workers et utilise des techniques de durcissement de l’environnement JavaScript, je pense que la manière la plus robuste d’isoler consiste à interpréter JavaScript, c’est-à-dire du JavaScript dans du JavaScript ou, comme ici, du JavaScript dans du WASM
    Cela évite aussi une exposition directe aux bugs de la machine virtuelle JavaScript hôte elle-même. Si l’on cible Node, c’est peut-être encore plus important, car en dehors de certaines tendances récentes, Node.js ne semble pas, contrairement à Deno, avoir été conçu avec l’isolation et le sandboxing à l’esprit
    En regardant l’API, je ne vois pas si createRuntime permet de définir des appels vers l’environnement hôte en dehors de fetch. Ce serait pourtant une fonctionnalité très utile, car elle permettrait de limiter les interactions avec le monde extérieur de manière contrôlée, au lieu de tout autoriser ou tout bloquer
    De même, il ne semble pas non plus y avoir de prise en charge du navigateur. Du moins, c’est ce que j’ai constaté rapidement avec esm.sh. Cela pourrait aussi être une fonctionnalité utile
    Je vais tester pour voir quel est le niveau de surcharge, et comme je l’ai dit, l’approche en elle-même semble assez solide
    [1] @exact-realty/lot

  • On dirait qu’on dit simplement que les CPU sont devenus tellement rapides qu’il est temps de faire tourner un interpréteur dans un interpréteur

  • Je ne mettrais pas en avant les performances comme avantage d’exécuter du JavaScript dans QuickJS. QuickJS n’est absolument pas compétitif face à une machine virtuelle JavaScript hôte
    En revanche, il peut être plus rapide que d’anciens interpréteurs en C ou des interpréteurs implémentés en JavaScript

    • Si le temps de démarrage du processus Node.js domine le temps d’exécution du script, il peut y avoir un gain de performance. Je pense que cela s’applique probablement à pas mal de scripts de type fonctions serverless
  • Ça permettrait d’exécuter du code JavaScript fourni par l’utilisateur. Je cherchais justement un moyen de regrouper du code TypeScript utilisateur avec un bundler dans un environnement sandboxé
    Je serais curieux d’avoir des recommandations sur la manière d’exécuter un bundler comme webpack dans QuickJS

    • Je ne sais pas comment faire avec QJS, mais si tu veux exécuter un bundler dans le navigateur, WebContainers semble avoir été conçu exactement pour ça
      [1]: https://webcontainers.io/
  • Très cool. Si c’est compilé en WASM, je me demande si cela peut s’exécuter dans le navigateur. Si c’est possible tout en permettant d’effectuer des requêtes fetch sans y joindre de cookies, ce serait intéressant

  • Dans mon précédent travail, il y avait bien trop de segmentation faults et d’erreurs silencieuses avec Quickjs-emscripten, au point que le projet a été mis en pause
    Si je devais recommencer, j’utiliserais probablement un moteur qui fonctionne correctement dans davantage de programmes et qui dispose d’un bundle WASM officiellement approuvé

  • Je pensais qu’on pouvait sandboxer du code en toute sécurité avec un iframe, mais je n’en suis pas certain. Bien sûr, avec un interpréteur dédié, on peut probablement ajouter davantage de fonctionnalités comme des limites de temps plus fines ou des API personnalisées

    • Moi aussi, surtout si on ajoute en plus des service workers, j’aimerais mieux comprendre ce point
      Pour l’instant, j’utilise un iframe qui reçoit le code via window message, puis évalue le code d’entrée avant de renvoyer une réponse via window message, et ça fonctionne plutôt bien. En revanche, je ne sais pas s’il existe une faille permettant de s’échapper du sandbox
      Pour des cas plus complexes, comme les dépendances de paquets, j’ai essayé d’analyser le code avec Babel puis de créer une import map utilisant un CDN (esm.sh), mais il arrivait que les versions fournies via le CDN ne fonctionnent pas correctement
      J’ai donc utilisé StackBlitz, et cela fonctionne assez bien, mais il y a quelques problèmes dans les environnements qui ne sont pas des contextes sécurisés
      J’ai fini par créer un petit serveur web qui reçoit le code et les dépendances (package.json), effectue un build Vite dans un conteneur Docker, puis renvoie le résultat. Ça marche plutôt bien, mais c’est parfois lent
      Ce serait bien de pouvoir tout builder côté client, et StackBlitz fait en gros ce genre de chose
    • Les iframe sont trop permissifs. Par exemple, un iframe peut exfiltrer des données vers un tiers