IMPORTANTE: Trata-se de executar apenas código JavaScript e TypeScript. Dito isto, a escrita também pode ser a direção para executar outro código em outras linguagens.
Permitir que os usuários executem seu código em seu aplicativo abre um mundo de personalização e funcionalidade, mas também expõe sua plataforma a ameaças de segurança significativas.
Dado que se trata de código de usuário, tudo é esperado, desde a parada dos servidores (podem ser loops infinitos) até o roubo de informações confidenciais.
Este artigo explorará várias estratégias para mitigar a execução do código do usuário, incluindo Web Workers, análise de código estático e muito mais…
Existem muitos cenários em que você precisa executar código fornecido pelo usuário, desde ambientes de desenvolvimento colaborativo como CodeSandbox e StackBiltz até plataformas de API personalizáveis como January. Até mesmo os playgrounds de código são suscetíveis a riscos.
Ou seja, as duas vantagens essenciais de executar com segurança o código fornecido pelo usuário são:
A execução do código do usuário não é prejudicial até que você esteja preocupado com a possibilidade de que alguns dados sejam roubados. Quaisquer dados que o preocupem serão considerados informações confidenciais. Por exemplo, na maioria dos casos, JWT é uma informação sensível (talvez quando usado como um mecanismo de autenticação)
Considere os riscos potenciais do JWT armazenado em cookies enviados com cada solicitação. Um usuário pode acionar inadvertidamente uma solicitação que envia o JWT para um servidor malicioso e...
O mais simples de todos, porém o mais arriscado.
eval('console.log("I am dangerous!")');
Quando você executa esse código, ele registra essa mensagem. Essencialmente, eval é um intérprete JS capaz de acessar o escopo global/janela.
const res = await eval('fetch(`https://jsonplaceholder.typicode.com/users`)'); const users = await res.json();
Este código usa fetch que é definido no escopo global. O intérprete não sabe disso, mas como eval pode acessar uma janela, ele sabe. Isso implica que executar uma avaliação no navegador é diferente de executá-la em um ambiente de servidor ou trabalhador.
eval(`document.body`);
Que tal agora...
eval(`while (true) {}`);
Este código interromperá a guia do navegador. Você pode perguntar por que um usuário faria isso consigo mesmo. Bem, eles podem estar copiando código da Internet. É por isso que é preferível fazer análises estáticas com/ou cronometrar a execução.
Você pode querer verificar os documentos do MDN sobre eval
A execução do time box pode ser feita executando o código em um web trabalhador e usando setTimeout para limitar o tempo de execução.
async function timebox(code, timeout = 5000) { const worker = new Worker('user-runner-worker.js'); worker.postMessage(code); const timerId = setTimeout(() => { worker.terminate(); reject(new Error('Code execution timed out')); }, timeout); return new Promise((resolve, reject) => { worker.onmessage = event => { clearTimeout(timerId); resolve(event.data); }; worker.onerror = error => { clearTimeout(timerId); reject(error); }; }); } await timebox('while (true) {}');
Isso é semelhante a eval, mas é um pouco mais seguro, pois não pode acessar o escopo anexo.
const userFunction = new Function('param', 'console.log(param);'); userFunction(2);
Este código registrará 2.
Nota: O segundo argumento é o corpo da função.
O construtor da função não pode acessar o escopo envolvente, então o código a seguir gerará um erro.
function fnConstructorCannotUseMyScope() { let localVar = 'local value'; const userFunction = new Function('return localVar'); return userFunction(); }
Mas ele pode acessar o escopo global para que o exemplo de busca acima funcione.
Você pode executar “Function Constructor e eval em um WebWorker, o que é um pouco mais seguro devido ao fato de não haver acesso DOM.
Para implementar mais restrições, considere proibir o uso de objetos globais como fetch, XMLHttpRequest, sendBeacon Verifique este artigo sobre como você pode fazer isso.
Isolated-VM é uma biblioteca que permite executar código em uma VM separada (interface Isolate da v8)
import ivm from 'isolated-vm'; const code = `count = 5;`; const isolate = new ivm.Isolate({ memoryLimit: 32 /* MB */ }); const script = isolate.compileScriptSync(code); const context = isolate.createContextSync(); const jail = context.global; jail.setSync('log', console.log); context.evalSync('log("hello world")');
Este código registrará olá mundo
Esta é uma opção interessante, pois fornece um ambiente de área restrita para executar código. Uma ressalva é que você precisa de um ambiente com ligações Javascript. No entanto, um projeto interessante chamado Extism facilita isso. Você pode querer seguir o tutorial deles.
O que é fascinante nisso é que você usará eval para executar o código, mas dada a natureza do WebAssembly, DOM, rede, sistema de arquivos e acesso ao ambiente host não são possíveis (embora possam diferir com base em o tempo de execução do wasm).
function evaluate() { const { code, input } = JSON.parse(Host.inputString()); const func = eval(code); const result = func(input).toString(); Host.outputString(result); } module.exports = { evaluate };
Você terá que compilar o código acima primeiro usando Extism, que gerará um arquivo Wasm que pode ser executado em um ambiente que tenha Wasm-runtime (navegador ou node.js).
const message = { input: '1,2,3,4,5', code: ` const sum = (str) => str .split(',') .reduce((acc, curr) => acc parseInt(curr), 0); module.exports = sum; `, }; // continue running the wasm file
Agora estamos migrando para o lado do servidor, o Docker é uma ótima opção para executar código isoladamente da máquina host. (Cuidado com a fuga do contêiner)
Você pode usar o dockerode para executar o código em um contêiner.
import Docker from 'dockerode'; const docker = new Docker(); const code = `console.log("hello world")`; const container = await docker.createContainer({ Image: 'node:lts', Cmd: ['node', '-e', code], User: 'node', WorkingDir: '/app', AttachStdout: true, AttachStderr: true, OpenStdin: false, AttachStdin: false, Tty: true, NetworkDisabled: true, HostConfig: { AutoRemove: true, ReadonlyPaths: ['/'], ReadonlyRootfs: true, CapDrop: ['ALL'], Memory: 8 * 1024 * 1024, SecurityOpt: ['no-new-privileges'], }, });
Lembre-se de que você precisa ter certeza de que o servidor tem o docker instalado e em execução. Eu recomendo ter um servidor separado dedicado apenas a isso, que atue como um servidor de função pura.
Além disso, você pode se beneficiar ao dar uma olhada no sysbox, um tempo de execução de contêiner semelhante a VM que fornece um ambiente mais seguro. Sysbox vale a pena, especialmente se o aplicativo principal estiver sendo executado em um contêiner, o que significa que você executará o Docker no Docker.
Esse foi o método escolhido em janeiro, mas logo os recursos da linguagem exigiam mais do que passar o código pelo shell do contêiner. Além disso, por algum motivo, a memória do servidor aumenta frequentemente; executamos o código dentro de contêineres auto-removíveis a cada 1s de pressionamento de tecla rebatido. (Você pode fazer melhor!)
Gosto particularmente do Firecracker, mas é um pouco trabalhoso de configurar, então se você ainda não tem tempo, quer estar no lado seguro, faça uma combinação de análise estática e execução de time-boxing . Você pode usar o esprima para analisar o código e verificar qualquer ato malicioso.
Bem, a mesma história com uma etapa extra (pode ser opcional): Transpile o código para JavaScript antes de executá-lo. Simplificando, você pode usar o compilador esbuild ou TypeScript e continuar com os métodos acima.
async function build(userCode: string) { const result = await esbuild.build({ stdin: { contents: `${userCode}`, loader: 'ts', resolveDir: __dirname, }, inject: [ // In case you want to inject some code ], platform: 'node', write: false, treeShaking: false, sourcemap: false, minify: false, drop: ['debugger', 'console'], keepNames: true, format: 'cjs', bundle: true, target: 'es2022', plugins: [ nodeExternalsPlugin(), // make all the non-native modules external ], }); return result.outputFiles![0].text; }
Notas:
Além disso, você pode evitar a transpilação executando o código usando Deno ou Bun em um contêiner docker, pois eles suportam TypeScript pronto para uso.
Executar o código do usuário é uma faca de dois gumes. Ele pode fornecer muitas funcionalidades e personalização à sua plataforma, mas também expõe você a riscos de segurança significativos. É fundamental compreender os riscos e tomar as medidas adequadas para mitigá-los e lembrar que quanto mais isolado o ambiente, mais seguro ele é.
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3