1 puntos por GN⁺ 2024-07-08 | 1 comentarios | Compartir por WhatsApp
  • @sebastianwessel/quickjs es 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 y expect basado en Chai
  • En los ejemplos de uso, primero se carga una vez el wasm de QuickJS con loadQuickJs(), y luego se ejecuta código con runSandboxed() y evalCode(), pasando opciones como allowFetch, allowFs y env
  • 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/quickjs es 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 fetch para realizar llamadas http(s)
  • Soporte para pruebas: incluye un runner de pruebas y expect basado 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 variant desde @jitl/quickjs-ng-wasmfile-release-sync, y se usan loadQuickJs y SandboxOptions desde @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 posible
  • loadQuickJs() devuelve runSandboxed, que luego se usa para ejecutar el sandbox
  • El ejemplo de SandboxOptions incluye las siguientes opciones
    • allowFetch: true: inyecta fetch en el código y permite solicitar datos
    • allowFs: true: monta un sistema de archivos virtual y proporciona el módulo node:fs
    • env: 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 join desde path y llama a join('src','dist') para imprimir src/dist en el log del sistema host
  • Lee env.MY_ENV_VAR y muestra "env var value" en el log del sistema host
  • Crea una URL con new URL('https://example.com'), llama a fetch(url) y luego devuelve el resultado de f.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

 
GN⁺ 2024-07-08
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 fetch con las mismas cookies que la función fetch del host. No se debería activar fetch y 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

    • Me pregunto si usando un iframe se puede evitar el problema de las cookies de fetch, independientemente de si se usa esta biblioteca. O quisiera saber si sigue siendo un problema incluso dentro de un iframe.
  • 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.

    • Salesforce maneja esto con una combinación de web components y un ShadowRoot parcheado. Evita que el código que tiene una referencia al shadow root pueda salirse al resto del documento, y limita los objetos globales a los que pueden acceder scripts no confiables mediante una función de evaluación segura relacionada con SES (Secure EcmaScript).
      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
    • Lo más cercano es el prototipo jsmirrors de Allen Wirfs-Brock, pero entiendo que no llegó a especificarse para el DOM y, de entrada, tampoco había mucha intención de hacerlo. Solo trataba capabilities para el sistema de programación que es JavaScript.
      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 a document.createElement, sino una capability add-button. Así que implementa addButton.
      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.
    • Para dar acceso de forma realmente segura, la única opción es darle al código de terceros un iframe en otro dominio y configurar el atributo sandbox.
      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...
    • Una forma posible sería agregar una API en la que el tercero envíe como string el HTML que quiere renderizar, parsearlo con alguna de varias librerías y luego reconstruir el DOM a partir de los datos parseados.
      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.
    • Pensándolo sobre la marcha, se podría hacer algo así: desde el backend, solicitar el código de terceros y asociar ese código con una secuencia de hash.
      Desde el backend, modificar dinámicamente el HTML para insertar una etiqueta div cuyo valor de id sea la secuencia de hash. También modificar el HTML para insertar una etiqueta script que solicite el código de terceros desde tu propio dominio y, para seguimiento, agregar el valor hash en un atributo data de esa etiqueta script.

En el backend, modificar el código de terceros para reemplazar todas las instancias de document. y window. por document.getElementById(hash_value)., y hacer que todos los selectores de consulta comiencen con #hash_value
Reemplazar .parentNode del prototipo Element por una propiedad personalizada, para verificar y bloquear los intentos de salir del contenedor proporcionado
Luego 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 createRuntime permite definir llamadas al entorno host además de fetch. 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

  • 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.

    • Si el tiempo de arranque del proceso de Node.js domina más que el tiempo de ejecución del script, podría haber una ganancia de rendimiento. Probablemente esto aplique a bastantes scripts del tipo funciones serverless.
  • 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.

    • No sé cómo hacerlo con QJS, pero si quieres ejecutar un bundler en el navegador, WebContainers parece estar hecho justamente para eso.
      [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 fetch sin 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.

    • Yo también quisiera saber más sobre esto, especialmente si se mezclan service workers.
      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.
    • Los iframe tienen demasiados permisos. Por ejemplo, un iframe puede filtrar datos a terceros.