"Se um trabalhador quiser fazer bem o seu trabalho, ele deve primeiro afiar suas ferramentas." - Confúcio, "Os Analectos de Confúcio. Lu Linggong"
Primeira página > Programação > Executando código JavaScript não confiável

Executando código JavaScript não confiável

Publicado em 2024-07-29
Navegar:622

Running Untrusted JavaScript Code

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…

Você deveria se importar

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:

  1. Ganhando a confiança do usuário: mesmo que o usuário seja confiável, ele pode executar código copiado de outras pessoas intencionalmente más.
  2. Proteja seu ambiente: a última coisa que você precisa é de um trecho de código que interrompa seu servidor. Pense enquanto (verdadeiro) {}

Defina “informações confidenciais”

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)

O que poderia dar errado

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

  • Scripting entre sites (XSS).
  • Ataques de negação de serviço (DoS).
  • Exfiltração de dados. Sem as proteções adequadas, essas ameaças podem comprometer a integridade e o desempenho da sua aplicação.

Métodos

O Malvado Avaliador

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) {}');

Construtor de função

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.

WebWorker

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.

VM isolada

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

WebAssembly

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

Docker

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!)

Outras opções

  • Contêineres da Web
  • MicroVM (Foguetão)
  • Sub-hospedagem Deno
  • Wasmer
  • Reinos das Sombras

Opção mais segura

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.

Como executar o código TypeScript?

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:

  • Os empacotadores baseados em Rust geralmente oferecem uma versão web assembly, o que significa que você pode transpilar o código no navegador. Esbuild tem uma versão web assembly.
  • Não inclua importações especificadas pelo usuário no pacote, a menos que você as tenha incluído na lista de permissões.

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.

Conclusão

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

Referências

  • Compilação instantânea de janeiro
  • Executando JavaScript não confiável em Node.js
  • Como as linguagens suportam a execução de código de usuário não confiável em tempo de execução?
  • Avaliando JavaScript com segurança com dados de contexto
Declaração de lançamento Este artigo foi reproduzido em: https://dev.to/this-is-learning/running-untrusted-javascript-code-3mi3?1 Se houver alguma violação, entre em contato com [email protected] para excluí-la
Tutorial mais recente Mais>

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