1 pontos por GN⁺ 2024-07-08 | 1 comentários | Compartilhar no WhatsApp
  • @sebastianwessel/quickjs é um pacote TypeScript que executa com segurança código JavaScript e TypeScript dentro de um sandbox que compila o motor QuickJS para WebAssembly
  • Seu foco é executar código não confiável de forma isolada, oferecendo um ambiente de execução leve e rápido por meio do sandbox WebAssembly do QuickJS
  • Inclui suporte a módulos básicos do Node.js, montagem de um sistema de arquivos virtual, montagem de módulos Node personalizados, cliente fetch, runner de testes e expect baseado no Chai
  • O exemplo de uso carrega o wasm do QuickJS uma vez com loadQuickJs() e depois executa código com runSandboxed() e evalCode(), passando opções como allowFetch, allowFs e env
  • Desenvolvedores que precisam executar código JavaScript externo em aplicações TypeScript podem usá-lo como um ambiente de execução que combina isolamento, desempenho e simplicidade de API

Pacote de sandbox WebAssembly do QuickJS

  • @sebastianwessel/quickjs é um pacote TypeScript que permite executar código JavaScript e TypeScript dentro de um sandbox WebAssembly
  • Ele usa o motor QuickJS compilado para WebAssembly e é voltado a executar código não confiável em um ambiente isolado
  • O pacote oferece um ambiente robusto para execução de código aproveitando o motor QuickJS, leve e rápido
  • Fornece documentação completa, exemplos e um playground online

Funcionalidades oferecidas

  • Execução segura: executa código JavaScript e TypeScript não confiável em um ambiente seguro e isolado
  • Módulos básicos do Node.js: oferece suporte a módulos padrão básicos do Node.js para casos de uso comuns
  • Sistema de arquivos: permite montar um sistema de arquivos virtual
  • Módulos Node personalizados: permite montar módulos Node definidos pelo usuário
  • Chamadas de rede: fornece um cliente fetch para realizar chamadas http(s)
  • Suporte a testes: inclui um runner de testes e expect baseado no Chai
  • Desempenho e integração: aproveita a leveza e eficiência do motor QuickJS e pode ser facilmente integrado a projetos TypeScript existentes
  • API simples: oferece uma API amigável para executar e gerenciar código JavaScript e TypeScript dentro do sandbox

Fluxo básico de uso

  • Primeiro, importe variant de @jitl/quickjs-ng-wasmfile-release-sync e use loadQuickJs e SandboxOptions de @sebastianwessel/quickjs
  • loadQuickJs(variant) carrega e inicializa o wasm do QuickJS; como essa operação é intensiva em recursos, recomenda-se fazê-la apenas uma vez sempre que possível
  • loadQuickJs() retorna runSandboxed, que é usado depois para executar no sandbox
  • Um exemplo de SandboxOptions inclui as seguintes opções
    • allowFetch: true: injeta fetch no código e permite solicitações de dados
    • allowFs: true: monta um sistema de arquivos virtual e fornece o módulo node:fs
    • env: passa valores de variáveis de ambiente para uso no código do sandbox

Ações possíveis no exemplo de execução

  • O código dentro do sandbox importa join de path e chama join('src','dist'), imprimindo src/dist no log do sistema host
  • env.MY_ENV_VAR e imprime "env var value" no log do sistema host
  • Cria uma URL com new URL('https://example.com'), chama fetch(url) e retorna o resultado de f.text()
  • Executa o código no formato runSandboxed(async ({ evalCode }) => evalCode(code), options)
  • Um exemplo de resultado tem o formato { ok: true, data: '<!doctype html>\n<html>\n[....]</html>\n' }

Licença e casos de uso adequados

  • Este projeto é disponibilizado sob a MIT License
  • É adequado para desenvolvedores que querem executar código JavaScript com segurança dentro de aplicações TypeScript
  • Por meio do sandbox WebAssembly do QuickJS, oferece um ambiente de execução que combina desempenho e segurança

1 comentários

 
GN⁺ 2024-07-08
Opiniões no Hacker News
  • Como autor da biblioteca de runtime quickjs-emscripten, gostei do estilo de biblioteca padrão ergonômica criado para o quickjs-emscripten
    Fico curioso se vocês testaram isso rodando no navegador ou em bundlers. A abordagem de receber o nome da variante como string e passá-lo dinamicamente para import(variantName) pode não funcionar bem com ferramentas como Webpack
    Alerta de segurança: esta biblioteca permite que código não confiável do guest chame fetch com os mesmos cookies da função fetch do host. Não se deve ativar fetch e executar código não confiável
    Se o código dentro da sandbox consegue chamar APIs HTTP arbitrárias autenticadas no contexto do host, então isso não é uma “sandbox”
    O motivo de o quickjs-emscripten ser de baixo nível e evitar recursos mágicos é poder afirmar com confiança que as APIs oferecidas são seguras. Em geral, recuso pedidos de serialização mágica ou de acesso fácil à rede/sistema de arquivos, porque esse tipo de código é uma área propensa a erros de segurança
    Ao executar código não confiável, é preciso auditar cuidadosamente não só a própria sandbox, mas também o código escrito para as APIs expostas a ela
    Percebi esse possível problema de segurança ao ver um comentário de outro usuário do HN perguntando sobre cookies no Fetch
    Leituras adicionais: artigo sobre a segurança da sandbox de plugins do Figma https://www.figma.com/blog/how-we-built-the-figma-plugin-sys..., https://www.figma.com/blog/an-update-on-plugin-security/, README do Quickjs-emscripten https://github.com/justjake/quickjs-emscripten

    • Fico curioso se, usando um iframe, seria possível evitar o problema dos cookies no fetch independentemente do uso desta biblioteca. Ou gostaria de saber se isso ainda continuaria sendo um problema mesmo em um iframe
  • Há muitas formas de fazer sandbox de JavaScript tanto no lado do servidor quanto no lado do navegador, mas não sei se existe uma forma de “isolar” o acesso ao DOM
    Por exemplo, dar a um terceiro não confiável acesso apenas a elementos do DOM em locais predefinidos. Pelo que sei, a única tecnologia que permite isso é iframe, mas infelizmente ele é pesado e lento
    Estou criando um app que pode hospedar plugins, e parece que, se eu der acesso ao DOM aos plugins, eles literalmente poderão fazer qualquer coisa

    • A Salesforce lida com isso combinando Web Components com um ShadowRoot modificado. Ela impede que código com uma referência ao shadow root “saia andando” para o restante do documento e, com uma função de avaliação segura relacionada ao SES (Secure EcmaScript), limita os objetos globais que scripts não confiáveis podem acessar
      Essa função de avaliação segura é bem impressionante. O núcleo provavelmente está aqui: https://github.com/Agoric/realms-shim/blob/v1.1.0/src/evalua...
      Também existe a ideia de Web Components isolados para resolver esse problema no nível da plataforma: https://github.com/WICG/webcomponents/issues/1002
    • O mais próximo disso é o protótipo jsmirrors de Allen Wirfs-Brock, mas, pelo que sei, ele não chegou a especificar nada para o DOM e nem havia muita intenção de fazer isso. Ele tratava apenas de capabilities para o sistema de programação que é o JavaScript
      Inspirando-se no jsmirrors, dá para tentar fazer algo como “dommirrors” por conta própria, mas é um trabalho grande. Também há um contorno usando o jsmirrors como está para conseguir, em certa medida, o que você quer, mas não é confortável de usar
      Dito isso, intermediar ou simular acesso ao DOM quase certamente não é o que você realmente quer. O melhor é entender o que você de fato quer permitir que a outra parte faça e então conceder apenas a capability que possibilita essa ação
      Por exemplo, se quiser permitir que ela adicione um botão em algum lugar, você pode achar que precisa dar um ponto de ancoragem na forma de um elemento pai e permitir que ela crie nós do DOM com document.createElement. Mas o que você realmente quer não é dar acesso a document.createElement, e sim a capability add-button. Então basta implementar addButton
      Mais: <https://news.ycombinator.com/item?id=30703531#30706060>
      É melhor não dar ouvidos a quem disser que CSP serve para isso. Não serve. Para ser preciso, acho que a CSP é um lixo mal projetado e hostil ao usuário até mesmo para o objetivo para o qual foi criada, e que não deveria ter sido implementada e estendida da forma como é hoje. Depender disso é arriscado
    • Para um acesso realmente seguro, a única forma é dar ao código de terceiros um iframe em outro domínio e configurar o atributo sandbox
      Na página pai, você pode medir o tamanho do conteúdo do DOM e ajustar o tamanho do iframe dentro de certos limites, então dá para integrá-lo de forma mais natural à UI do app
      Dependendo do nível de exposição entre usuários e do nível de confiança, é preciso tomar cuidado com tentativas de impersonação/phishing e clickjacking dentro do iframe. Idealmente, o frame deveria ser bloqueado para não fazer nenhuma requisição web, o que significa que nem imagens seriam carregadas
      Assim, por exemplo, mesmo que o usuário seja enganado e digite a senha em um formulário de senha falso, não haverá como exfiltrar os dados para fora do frame
      A principal forma de limitar os tipos de recursos que um iframe pode requisitar é a Content-Security-Policy, com a qual é possível desativar imagens, scripts etc. de terceiros
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameE...
      https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
      Também é preciso ativar outros atributos de sandbox e desativar o acesso a APIs do DOM sensíveis à privacidade, como 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...
    • Uma abordagem possível parece ser adicionar uma API em que terceiros enviem, como string, o HTML que querem renderizar, parsear isso com uma das várias bibliotecas e então recriar o DOM com base nos dados parseados
      Assim, dá para restringir a uma lista de permissão os elementos e atributos HTML aceitos. Se você também quiser permitir listeners de eventos nativos do DOM, fica mais complexo, mas não é impossível
      Seria necessária uma API que receba algo como a string do nome do evento e o id do elemento que deve receber o evento; então, ao detectar esse evento no DOM real, você teria de replicar o mesmo evento para dentro do sandbox JavaScript em uso. Dentro desse sandbox, deve haver acesso à API mencionada acima
    • Pensando de improviso, daria para tentar algo assim: no backend, solicitar o código de terceiros e associar esse código a uma sequência de hash
      No backend, modificar dinamicamente o HTML para inserir uma tag div cujo valor de id seja a sequência de hash. Também modificar o HTML para inserir uma tag script que solicita o código de terceiros a partir do seu próprio domínio e, para rastreamento, adicionar o valor do hash a um atributo data dessa tag script

No backend, modificar o código de terceiros para substituir todas as instâncias de document. e window. por document.getElementById(hash_value)., e fazer com que todos os seletores de consulta comecem com #hash_value
Substituir .parentNode no protótipo de Element por uma propriedade customizada, para verificar e bloquear tentativas de sair do contêiner fornecido
Em seguida, enviar o documento HTML para o navegador. Tudo bem se o código de terceiros quebrar. As restrições devem ser informadas aos terceiros, e eles devem testar o próprio código antes de enviá-lo ao servidor
O importante é apenas impedir que o código saia dinamicamente do contêiner fornecido. Essa parte deve ser testada regularmente para encontrar violações de segurança
Pode ser que, na prática, não funcione, mas parece divertido experimentar

  • Por acaso, testei o QuickJS na semana passada e acabei ficando com o isolated-vm. Ambos atendiam ao contrato de segurança, mas o isolated-vm teve desempenho muito melhor em termos de overhead de configuração, desmontagem e execução de eval

  • É uma abordagem interessante. Como autor de outra biblioteca de sandbox JavaScript[1] que isola via workers e usa técnicas de limpeza do ambiente JavaScript, acho que a forma de interpretar JavaScript — ou seja, JavaScript dentro de JavaScript, ou, como neste caso, JavaScript dentro de WASM — oferece o nível mais alto de isolamento
    Além disso, não fica diretamente exposta a bugs da própria máquina virtual JavaScript do host. Se o alvo for Node, isso pode ser ainda mais importante, porque, exceto por alguns fluxos mais recentes, o Node.js não parece ter sido projetado com isolamento e sandboxing em mente, diferentemente do Deno
    Olhando a API, não parece que createRuntime possa definir chamadas ao ambiente host além de fetch. Esse recurso seria bem útil, porque permitiria limitar a comunicação com o mundo externo de forma controlada, em vez de permitir tudo ou bloquear tudo
    Da mesma forma, também parece não dar suporte a navegadores. Pelo menos foi o que vi em uma checagem rápida com esm.sh. Isso também poderia ser um recurso útil
    Estou curioso para saber qual é o overhead e pretendo testar; como disse, a abordagem em si parece bastante sólida
    [1] @exact-realty/lot

  • Parece que a ideia é: as CPUs ficaram rápidas demais, então agora vamos rodar um interpretador dentro de outro interpretador

  • Eu não apontaria desempenho como uma vantagem de executar JavaScript no QuickJS. O QuickJS não compete de forma alguma com a máquina virtual JavaScript do host
    Ainda assim, pode ser mais rápido que interpretadores antigos em C ou interpretadores implementados em JavaScript

    • Se o tempo de inicialização do processo Node.js domina mais do que o tempo de execução do script, pode haver ganho de desempenho. Acho que uma boa parte dos scripts do tipo funções serverless se encaixa nisso
  • Com isso dá para executar código JavaScript fornecido por usuários. Eu estava procurando uma forma de empacotar código TypeScript do usuário com um bundler em um ambiente sandbox
    Fico curioso se há alguma recomendação sobre como executar um bundler como o webpack dentro do QuickJS

    • Não sei como fazer com QJS, mas, se você quer executar um bundler no navegador, os WebContainers parecem ter sido feitos exatamente para isso
      [1]: https://webcontainers.io/
  • Muito legal. Se foi compilado para WASM, fico curioso se dá para executar no navegador. Se for possível e ainda der para fazer requisições fetch sem anexar cookies às solicitações, seria interessante

  • No meu emprego anterior, tivemos tantos segmentation faults e erros silenciosos no Quickjs-emscripten que o projeto ficou em espera
    Se eu tivesse que fazer de novo, acho que usaria uma engine que funcionasse corretamente em mais programas e tivesse um bundle WASM oficialmente aprovado

  • Eu achava que era possível colocar código em sandbox com segurança usando iframe, mas não tenho certeza. Claro que, usando um interpretador próprio, parece dar para incluir mais recursos, como limites de tempo mais granulares e APIs customizadas

    • Eu também queria entender melhor essa parte, especialmente quando service workers entram na mistura
      Hoje uso um iframe que recebe código via window message e, depois de avaliar o código de entrada, consegue devolver a resposta por window message; funciona bem. Mas não tenho certeza se há alguma brecha para escapar da sandbox
      Em casos mais complexos, por exemplo dependências de pacotes, tentei fazer o parsing com Babel e depois criar um import map usando um CDN (esm.sh), mas houve casos em que a versão em CDN não funcionava bem
      Então usei o StackBlitz, e funciona bem, mas há alguns problemas em ambientes que não são contextos seguros
      No fim, criei um pequeno servidor web que recebe o código e as dependências (package.json), executa um build do Vite dentro de um contêiner Docker e devolve o resultado. Funciona razoavelmente bem, mas às vezes é lento
      Seria bom poder fazer o build totalmente no cliente, e o StackBlitz faz algo mais ou menos nessa linha
    • iframes têm permissões concedidas em excesso. Por exemplo, um iframe pode exfiltrar dados para terceiros