HN Show: Execute JavaScript em um sandbox WebAssembly QuickJS
(github.com/sebastianwessel)@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 eexpectbaseado no Chai - O exemplo de uso carrega o wasm do QuickJS uma vez com
loadQuickJs()e depois executa código comrunSandboxed()eevalCode(), passando opções comoallowFetch,allowFseenv - 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
fetchpara realizar chamadas http(s) - Suporte a testes: inclui um runner de testes e
expectbaseado 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
variantde@jitl/quickjs-ng-wasmfile-release-synce useloadQuickJseSandboxOptionsde@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ívelloadQuickJs()retornarunSandboxed, que é usado depois para executar no sandbox- Um exemplo de
SandboxOptionsinclui as seguintes opçõesallowFetch: true: injetafetchno código e permite solicitações de dadosallowFs: true: monta um sistema de arquivos virtual e fornece o módulonode:fsenv: 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
joindepathe chamajoin('src','dist'), imprimindosrc/distno log do sistema host - Lê
env.MY_ENV_VARe imprime"env var value"no log do sistema host - Cria uma URL com
new URL('https://example.com'), chamafetch(url)e retorna o resultado def.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
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 WebpackAlerta de segurança: esta biblioteca permite que código não confiável do guest chame
fetchcom os mesmos cookies da funçãofetchdo host. Não se deve ativarfetche executar código não confiávelSe 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
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
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
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 adocument.createElement, e sim a capability add-button. Então basta implementaraddButtonMais: <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
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...
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
No backend, modificar dinamicamente o HTML para inserir uma tag
divcujo valor de id seja a sequência de hash. Também modificar o HTML para inserir uma tagscriptque 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 tagscriptNo backend, modificar o código de terceiros para substituir todas as instâncias de
document.ewindow.pordocument.getElementById(hash_value)., e fazer com que todos os seletores de consulta comecem com#hash_valueSubstituir
.parentNodeno protótipo deElementpor uma propriedade customizada, para verificar e bloquear tentativas de sair do contêiner fornecidoEm 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
createRuntimepossa definir chamadas ao ambiente host além defetch. 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 tudoDa 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
Também há APIs para expor funções do host, chamar funções do guest, loaders de módulos customizados etc.: https://github.com/justjake/quickjs-emscripten?tab=readme-ov...
Documentação da API
newFunction: https://github.com/justjake/quickjs-emscripten/blob/main/doc...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
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
[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
fetchsem anexar cookies às solicitações, seria interessanteNo 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
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