HN : exécuter JavaScript dans un bac à sable QuickJS WebAssembly
(github.com/sebastianwessel)@sebastianwessel/quickjsest 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 unexpectbasé sur Chai - Les exemples d’utilisation consistent à charger une fois le wasm QuickJS avec
loadQuickJs(), puis à exécuter le code avecrunSandboxed()etevalCode(), en passant des options commeallowFetch,allowFsetenv - 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/quickjsest 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
fetchpour effectuer des appels http(s) - Prise en charge des tests : inclut un runner de tests et un
expectbasé 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
variantdepuis@jitl/quickjs-ng-wasmfile-release-sync, puis utiliserloadQuickJsetSandboxOptionsdepuis@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 foisloadQuickJs()renvoierunSandboxed, qui est ensuite utilisé pour l’exécution dans le bac à sable- Un exemple de
SandboxOptionsinclut les options suivantesallowFetch: true: injectefetchdans le code et autorise les requêtes de donnéesallowFs: true: monte un système de fichiers virtuel et fournit le modulenode:fsenv: 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
joindepuispathet appellejoin('src','dist'), ce qui affichesrc/distdans les logs du système hôte - Il lit
env.MY_ENV_VARet affiche"env var value"dans les logs du système hôte - Il crée une URL avec
new URL('https://example.com'), appellefetch(url), puis renvoie le résultat def.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
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 WebpackAvertissement de sécurité : cette bibliothèque permet à du code invité non fiable d’appeler
fetchavec les mêmes cookies que la fonctionfetchde l’hôte. Il ne faut pas activerfetchet exécuter du code non fiableSi 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
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 lentJe 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
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
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émenteraddButtonPour 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é
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...
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
Le backend modifie alors dynamiquement le HTML pour y insérer une balise
divdont la valeur deidest cette séquence de hachage. Il modifie aussi le HTML pour insérer une balisescriptqui récupère le code tiers depuis son propre domaine, et ajoute la valeur de hachage dans un attribut data de cette balisescriptà des fins de suiviCôté backend, modifier le code tiers pour remplacer toutes les occurrences de
document.etwindow.pardocument.getElementById(hash_value)., et faire en sorte que tous les sélecteurs de requête commencent par#hash_valueRemplacer
.parentNodedu prototypeElementpar une propriété personnalisée, afin de pouvoir vérifier et bloquer les tentatives de sortie du conteneur fourniEnvoyer 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
evalApproche 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
createRuntimepermet de définir des appels vers l’environnement hôte en dehors defetch. 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 bloquerDe 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
Elle propose aussi des API pour exposer des fonctions hôtes, appeler des fonctions invitées et définir des chargeurs de modules personnalisés : https://github.com/justjake/quickjs-emscripten?tab=readme-ov...
Documentation de l’API
newFunction: https://github.com/justjake/quickjs-emscripten/blob/main/doc...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
Ç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
[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
fetchsans y joindre de cookies, ce serait intéressantDans 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
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 lentCe serait bien de pouvoir tout builder côté client, et StackBlitz fait en gros ce genre de chose