HN en abierto: Ejecutar JavaScript en un sandbox de QuickJS con WebAssembly
(github.com/sebastianwessel)@sebastianwessel/quickjses un paquete de TypeScript que permite ejecutar de forma segura código JavaScript y TypeScript dentro de un sandbox con QuickJS compilado a WebAssembly- Se centra en aislar y ejecutar código no confiable, y ofrece un entorno de ejecución ligero y rápido mediante un sandbox de QuickJS WebAssembly
- Incluye soporte para módulos básicos de Node.js, montaje de sistema de archivos virtual, montaje de módulos Node personalizados, cliente
fetch, runner de pruebas yexpectbasado en Chai - En los ejemplos de uso, primero se carga una vez el wasm de QuickJS con
loadQuickJs(), y luego se ejecuta código conrunSandboxed()yevalCode(), pasando opciones comoallowFetch,allowFsyenv - Los desarrolladores que necesiten ejecutar código JavaScript externo desde aplicaciones TypeScript pueden usarlo como un entorno de ejecución que combina aislamiento, rendimiento y simplicidad de API
Paquete de sandbox de QuickJS WebAssembly
@sebastianwessel/quickjses un paquete de TypeScript que permite ejecutar código JavaScript y TypeScript dentro de un sandbox de WebAssembly- Usa el motor QuickJS compilado a WebAssembly y está orientado a ejecutar código no confiable en un entorno aislado
- El paquete aprovecha el motor QuickJS, ligero y rápido, para ofrecer un entorno sólido para la ejecución de código
- Ofrece documentación completa, ejemplos y un playground en línea
Funcionalidades
- Ejecución segura: ejecuta código JavaScript y TypeScript no confiable en un entorno seguro y aislado
- Módulos básicos de Node.js: ofrece soporte para módulos estándar básicos de Node.js para casos de uso comunes
- Sistema de archivos: permite montar un sistema de archivos virtual
- Módulos Node personalizados: permite montar módulos Node definidos por el usuario
- Llamadas de red: ofrece un cliente
fetchpara realizar llamadas http(s) - Soporte para pruebas: incluye un runner de pruebas y
expectbasado en Chai - Rendimiento e integración: aprovecha la ligereza y eficiencia del motor QuickJS y se integra fácilmente en proyectos TypeScript existentes
- API simple: ofrece una API amigable para ejecutar y administrar código JavaScript y TypeScript dentro del sandbox
Flujo básico de uso
- Primero se importa
variantdesde@jitl/quickjs-ng-wasmfile-release-sync, y se usanloadQuickJsySandboxOptionsdesde@sebastianwessel/quickjs loadQuickJs(variant)carga e inicializa el wasm de QuickJS, y como esta tarea es intensiva en recursos, se recomienda realizarla solo una vez cuando sea posibleloadQuickJs()devuelverunSandboxed, que luego se usa para ejecutar el sandbox- El ejemplo de
SandboxOptionsincluye las siguientes opcionesallowFetch: true: inyectafetchen el código y permite solicitar datosallowFs: true: monta un sistema de archivos virtual y proporciona el módulonode:fsenv: pasa valores de variables de entorno para usar dentro del código del sandbox
Acciones posibles en el ejemplo de ejecución
- El código dentro del sandbox importa
joindesdepathy llama ajoin('src','dist')para imprimirsrc/disten el log del sistema host - Lee
env.MY_ENV_VARy muestra"env var value"en el log del sistema host - Crea una URL con
new URL('https://example.com'), llama afetch(url)y luego devuelve el resultado def.text() - Ejecuta el código con la forma
runSandboxed(async ({ evalCode }) => evalCode(code), options) - Un ejemplo de resultado tiene la forma
{ ok: true, data: '<!doctype html>\n<html>\n[....]</html>\n' }
Licencia y casos de uso adecuados
- Este proyecto se distribuye bajo la MIT License
- Es adecuado para desarrolladores que buscan ejecutar de forma segura código JavaScript dentro de aplicaciones TypeScript
- A través del sandbox de QuickJS WebAssembly, ofrece un entorno de ejecución que combina rendimiento y seguridad
1 comentarios
Opiniones de Hacker News
Como autor de la biblioteca de runtime quickjs-emscripten, me gusta el estilo ergonómico de biblioteca estándar que crearon para quickjs-emscripten.
Me pregunto si la probaron ejecutándola en un navegador o con un bundler. La forma de recibir el nombre de la variante como string y pasarlo dinámicamente a
import(variantName)puede no llevarse bien con herramientas como Webpack.Advertencia de seguridad: esta biblioteca permite que código no confiable del invitado llame a
fetchcon las mismas cookies que la funciónfetchdel host. No se debería activarfetchy ejecutar código no confiable.Si el código dentro del sandbox puede llamar a APIs HTTP arbitrarias autenticadas como el contexto host, eso no es un “sandbox”.
La razón por la que quickjs-emscripten es de bajo nivel y evita funciones mágicas es para poder afirmar con seguridad que las APIs que ofrece son seguras. En general rechazo pedidos de serialización mágica o de acceso fácil a red/sistema de archivos, porque este tipo de código es un terreno propenso a errores de seguridad.
Al ejecutar código no confiable, hay que auditar cuidadosamente no solo el sandbox en sí, sino también el código escrito para las APIs que se exponen al sandbox.
Me di cuenta del posible problema de seguridad al ver un comentario de otro usuario de HN que preguntaba por las cookies de Fetch.
Lecturas adicionales: artículos sobre la seguridad del sandbox de plugins de 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
Hay muchas formas de aislar JavaScript en un sandbox tanto del lado del servidor como del lado del navegador, pero no sé si existe alguna forma de “sandboxear” el acceso al DOM.
Por ejemplo, darle a un tercero no confiable acceso solo a elementos del DOM en una ubicación predefinida. Hasta donde sé, la única tecnología que permite esto es iframe, pero lamentablemente es pesado y lento.
Estoy creando una app que pueda alojar plugins, y si les doy acceso al DOM, parece que literalmente podrían hacer cualquier cosa.
Esa función de evaluación segura es bastante impresionante. Creo que el núcleo probablemente está aquí: https://github.com/Agoric/realms-shim/blob/v1.1.0/src/evalua...
También existe la idea de web components aislados para resolver este problema a nivel de plataforma: https://github.com/WICG/webcomponents/issues/1002
Uno podría intentar crear algo como “dommirrors” inspirado en jsmirrors, pero sería un trabajo grande. También hay formas alternativas de usar jsmirrors tal cual para lograr en cierta medida lo que quieres, pero no son cómodas de usar.
Dicho eso, mediar o simular acceso al DOM casi seguro no es lo que realmente quieres. Lo correcto es identificar qué quieres permitirle hacer realmente a la otra parte y darle solo la capability que habilita esa acción.
Por ejemplo, si quieres que pueda agregar un botón en algún lugar, podrías pensar que tienes que darle como punto de anclaje un elemento padre y permitirle crear nodos DOM con
document.createElement. Pero lo que realmente quieres no es darle acceso adocument.createElement, sino una capability add-button. Así que implementaaddButton.Más información: <https://news.ycombinator.com/item?id=30703531#30706060>
Conviene no hacerle caso a quien diga que CSP sirve para esto. No es así. Para ser precisos, creo que CSP es basura mal diseñada y hostil al usuario incluso para el propósito para el que fue creada, y nunca debió implementarse ni extenderse como se hizo. Depender de eso es peligroso.
Desde la página padre se puede medir el tamaño del contenido del DOM y ajustar el tamaño del iframe dentro de ciertos límites, así que puede integrarse de forma más natural en la UI de la app.
Según el nivel de exposición entre usuarios y el nivel de confianza, hay que tener cuidado con intentos de suplantación/phishing y clickjacking dentro del iframe. Idealmente, el frame debería quedar bloqueado para que no pueda hacer ninguna solicitud web, lo que también implica que no pueda cargar imágenes.
Así, por ejemplo, aunque se engañe al usuario para que escriba su contraseña en un formulario falso, no habría forma de filtrar los datos fuera del frame.
La principal forma de limitar los tipos de recursos que puede solicitar un iframe es Content-Security-Policy, con lo cual se pueden desactivar imágenes, scripts, etc. de terceros.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameE...
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
También hay que activar otros atributos sandbox y desactivar el acceso a APIs del DOM sensibles para la privacidad, como 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...
Así se pueden limitar con una lista blanca los elementos y atributos HTML permitidos. Permitir incluso la recepción de eventos nativos del DOM se vuelve más complejo, pero no es imposible.
Haría falta algo como una API que reciba el string con el nombre del evento y el id del elemento que recibirá el evento; luego habría que detectar ese evento en el DOM real y replicar el mismo evento dentro del sandbox de JavaScript que se esté usando. Dentro de ese sandbox tendría que haber acceso a la API mencionada.
Desde el backend, modificar dinámicamente el HTML para insertar una etiqueta
divcuyo valor de id sea la secuencia de hash. También modificar el HTML para insertar una etiquetascriptque solicite el código de terceros desde tu propio dominio y, para seguimiento, agregar el valor hash en un atributo data de esa etiquetascript.En el backend, modificar el código de terceros para reemplazar todas las instancias de
document.ywindow.pordocument.getElementById(hash_value)., y hacer que todos los selectores de consulta comiencen con#hash_valueReemplazar
.parentNodedel prototipoElementpor una propiedad personalizada, para verificar y bloquear los intentos de salir del contenedor proporcionadoLuego enviar el documento HTML al navegador. No importa si el código de terceros se rompe. Hay que informarles las restricciones a los terceros, y ellos deben probar su propio código antes de enviarlo al servidor
Lo importante es únicamente impedir que el código pueda salir del contenedor proporcionado dinámicamente. Esta parte debe probarse con regularidad para encontrar violaciones de seguridad
Puede que en la práctica no funcione, pero parece divertido experimentarlo
Por casualidad probé QuickJS la semana pasada y al final me quedé con isolated-vm. Ambos cumplían con el contrato de seguridad, pero isolated-vm fue mucho más eficiente en cuanto a sobrecarga de configuración, desmontaje y ejecución de
eval.Es un enfoque interesante. Como autor de otra biblioteca de sandbox para JavaScript[1] que aísla mediante workers y usa técnicas de limpieza del entorno de JavaScript, creo que la forma de interpretar JavaScript —es decir, JavaScript dentro de JavaScript, o JavaScript dentro de WASM como en este caso— ofrece el nivel más alto de aislamiento.
Además, no queda expuesto directamente a bugs de la propia máquina virtual de JavaScript del host. Si el objetivo es Node, esto puede ser aún más importante, porque salvo algunas tendencias recientes, Node.js no parece haber sido diseñado pensando en aislamiento y sandboxing, a diferencia de Deno.
Viendo la API, no se ve si
createRuntimepermite definir llamadas al entorno host además defetch. Esta funcionalidad sería bastante útil, porque permitiría limitar la comunicación con el mundo exterior de forma controlada, en lugar de permitirla toda o bloquearla toda.De forma similar, tampoco parece soportar navegadores. Al menos eso vi al hacer una prueba rápida con esm.sh. También podría ser una función útil.
Me intriga cuánta sobrecarga tiene, así que pienso probarlo; como dije, el enfoque en sí parece bastante sólido.
[1] @exact-realty/lot
También tiene APIs para exponer funciones del host, llamar funciones del invitado, cargadores de módulos personalizados, etc.: https://github.com/justjake/quickjs-emscripten?tab=readme-ov...
Documentación de la API
newFunction: https://github.com/justjake/quickjs-emscripten/blob/main/doc...Parece que, como las CPU se volvieron demasiado rápidas, ahora la idea es ejecutar un intérprete dentro de otro intérprete.
No diría que el rendimiento sea una ventaja de ejecutar JavaScript en QuickJS. QuickJS no compite en absoluto con la máquina virtual de JavaScript del host.
Sin embargo, puede ser más rápido que un intérprete viejo en C o que un intérprete implementado en JavaScript.
Con esto se podría ejecutar código JavaScript proporcionado por usuarios. Estaba buscando una forma de empaquetar con un bundler el código TypeScript de usuarios en un entorno sandbox.
Me pregunto si hay alguna recomendación sobre cómo ejecutar un bundler como webpack dentro de QuickJS.
[1]: https://webcontainers.io/
Muy genial. Si está compilado a WASM, me pregunto si se puede ejecutar en el navegador. Si se puede y además permite hacer solicitudes
fetchsin adjuntar cookies a las solicitudes, sería interesante.En mi trabajo anterior tuvimos tantos segmentation faults y errores silenciosos en Quickjs-emscripten que el proyecto quedó en pausa.
Si tuviera que hacerlo de nuevo, creo que usaría un motor que funcione correctamente con más programas y que tenga un bundle WASM aprobado oficialmente.
Pensaba que se podía sandboxear código de forma segura con un iframe, pero no estoy seguro. Claro que usando un intérprete propio parece posible agregar más funciones, como límites de tiempo más precisos o APIs personalizadas.
Ahora uso un iframe que recibe código por window message, evalúa el código de entrada y puede devolver la respuesta por window message, así que funciona bastante bien. Pero no estoy seguro de si habrá alguna vía para escaparse del sandbox.
Para casos más complejos, por ejemplo dependencias de paquetes, intenté parsearlas con Babel y luego crear un import map que use un CDN (esm.sh), pero hubo casos en que la versión servida por CDN no funcionaba bien.
Por eso usé StackBlitz, y funciona bastante bien, aunque tiene algunos problemas en entornos que no son contextos seguros.
Al final hice un pequeño servidor web que recibe el código y las dependencias (package.json), ejecuta una build de Vite dentro de un contenedor Docker y devuelve el resultado. Funciona razonablemente bien, pero a veces es lento.
Me gustaría poder compilar completamente del lado del cliente, y StackBlitz hace algo parecido.