Skip to main content

5 maneiras de evitar a injeção de código em JavaScript e Node.js

Escrito por
Prevent code injection vulnerabilities with Snyk

6 de abril de 2021

0 minutos de leitura

Escrever código JavaScript seguro para evitar a injeção de código pode parecer uma tarefa comum, mas há muitas armadilhas pelo caminho. Por exemplo, o fato de você (desenvolvedor) seguir as melhores práticas de segurança não significa que outras pessoas façam o mesmo. É provável que você use pacotes de código aberto no seu aplicativo. Como saber se eles foram desenvolvidos com segurança? E se houver código inseguro, como eval(), neles? Vamos descobrir.

O que é injeção de código?

A injeção de código é uma forma específica de ataques de injeção, uma categoria mais ampla, em que um invasor pode enviar código JavaScript ou Node.js que será interpretado pelo navegador ou pelo ambiente de execução do Node.js. A vulnerabilidade de segurança ocorre quando o interpretador não consegue distinguir o código confiável pretendido pelo desenvolvedor do código injetado pelo invasor como entrada.

Como evitar a injeção de código

Uma convenção essencial de programação segura é não permitir a execução dinâmica de código no aplicativo. Isso significa evitar construções da linguagem como eval e strings de código passadas para setTimeout() ou para o construtor Function. Em segundo lugar, evite a serialização, que pode ser vulnerável a ataques de injeção que executam código durante o processo. Por fim, faça a análise de dependências para garantir que componentes de código aberto de terceiros não deixem seu aplicativo vulnerável a esse ataque. Além disso, se você usar uma ferramenta de análise estática de código, como o Snyk Code, poderá encontrar possíveis vulnerabilidades de segurança relacionadas à injeção de código no seu código ou no de colegas.

Neste artigo, veremos 5 maneiras de evitar a injeção de código:

  1. Evite eval(), setTimeout() e setInterval()

  2. Evite new Function()

  3. Evite a serialização de código em JavaScript

  4. Use um linter de segurança para Node.js

  5. Use uma ferramenta de análise estática de código (SCA) para encontrar e corrigir problemas de injeção de código

1. Evite eval(), setTimeout() e setInterval()

Sei o que você está pensando: mais um guia mandando evitar eval. É verdade, mas também quero mostrar exemplos reais de outras bibliotecas populares que usaram eval (ou outras formas de construção de código), foram prejudicadas por isso e acabaram causando uma grave vulnerabilidade de segurança.

Antes de vermos os exemplos de pacotes de terceiros vulneráveis, vamos explicar o que é eval e quais funções estão relacionadas a ele. Ambientes de execução JavaScript, como o navegador e a plataforma Node.js no servidor, permitem avaliar e executar código durante a execução. Veja um exemplo prático:

const getElementForm = getElementType == “id” ? “getElementById” : “getElementByName”;
const priceTagValue = eval(“document.”+getElementForm+”(“+elementId+”).value”);

Nesse caso, o programador tenta criar uma maneira dinâmica de acessar dados no DOM. O exemplo pressupõe que getElementform pode ser controlado pelo usuário, assim como a variável elementId. Há maneiras melhores de realizar essa tarefa sem precisar de eval, então, de todo modo, você deve evitar código dinâmico como este.

No Node.js, pode ser necessário permitir o acesso a pontos de dados específicos do aplicativo com base em uma avaliação dinâmica. Veja um exemplo:

const db = "./db.json"
const dataPoints = eval("require('"+db+"')");

Neste exemplo, pressupõe-se que o arquivo específico que queremos importar seja dinâmico e potencialmente controlado pelo usuário, o que pode gerar vulnerabilidades de segurança relacionadas à injeção de código.

A injeção de código no Dustjs mostra um exemplo real de uso inseguro de eval

O pacote npm dustjs do LinkedIn, um projeto de templates assíncronos para o navegador e o Node.js no servidor, mostra como uma vulnerabilidade de injeção de código pode se tornar grave.

Embora esse pacote não seja mais mantido ativamente, ele ainda registra cerca de 72.000 downloads mensais e precisou lidar com uma vulnerabilidade de segurança relacionada à injeção de código.

Os mantenedores do dustjs fizeram o possível para escapar entradas potencialmente perigosas fornecidas pelo usuário, que poderiam chegar a construções de código inseguras como a função eval(). No entanto, a própria função escapeHtml tinha uma falha de segurança: ela verificava apenas se a entrada era uma string e, em seguida, fazia o escape. Também deveria verificar outros tipos, como arrays. Este pull request corrigiu a vulnerabilidade de segurança relacionada à injeção de código:

Diferença de código em lib/dust.js que atualiza dust.escapeHtml para lidar com valores semelhantes a strings antes de escapar o HTML

Você deve estar se perguntando: onde eval() entra nessa história?

Se você usa dustjs, talvez também adicione o pacote npm dustjs-helpers para obter helpers de template adicionais, como operações matemáticas e lógicas. Um desses helpers adicionais é uma condição if, que você pode usar nos seus próprios arquivos de template Dust da seguinte maneira:

Editor de código exibindo um template Dust com lógica condicional para diferentes tamanhos de texto do corpo em dispositivos desktop e móveis

Faz sentido, não é?

O problema é que a entrada não controlada do usuário no parâmetro de consulta device é passada diretamente ao helper da condição if, que usa eval, como você pode ver na linha 227, para avaliar a condição dinamicamente:

Editor de código com tema escuro exibindo código de uma função auxiliar em Dust.js com uma instrução eval e arquivos do projeto na barra lateral

Agora tudo fica claro: vários problemas de segurança se combinam de uma forma inesperada:

  1. dustjs-linkedin, um pacote de código aberto, tem uma falha de segurança: as strings de entrada não são sanitizadas corretamente na função escapeHtml.

  2. dustjs-helpers, um pacote de código aberto, usa uma prática de programação insegura, como a função eval(), para avaliar código dinamicamente durante a execução.

Quer ver como explorei essa vulnerabilidade e invadi um aplicativo real em funcionamento usando exatamente essa falha? Confira:

Liran Tal - Stranger Danger: Finding Security Vulnerabilities Before They Find You!

Evite também setTimeout() e setInterval()

Para concluir as boas práticas de evitar eval(), também quero destacar outras funções que você, como desenvolvedor JavaScript, certamente já ouviu falar ou usou pelo menos uma vez no seu aplicativo: setTimeout() e setInterval().

Um fato um pouco menos conhecido sobre essas funções é que elas também aceitam strings de código. Por exemplo, elas podem ser usadas assim:

setTimeout(“console.log(1+1)”, 1000);

Ainda bem que literais de string não são permitidos em um ambiente Node.js!

2. Evite new Function()

Outra construção da linguagem, semelhante a eval(), setTimeout() e setInterval(), é o construtor Function, que permite definir uma função dinamicamente com base em literais de string.

Veja este exemplo simples:

const addition = new Function(‘a’, ‘b’, ‘return a+b’);
addition(1, 1)

Se você acompanhou até aqui, já conhece os possíveis problemas de segurança que podem surgir quando uma entrada do usuário é passada para uma função como essa...

3. Evite a serialização de código em JavaScript

A serialização é bastante comum no ecossistema Java. Meu amigo Brian Vermeer escreveu um artigo sobre como vulnerabilidades de segurança afetam aplicativos Java devido a operações de serialização inseguras. Recomendo muito a leitura: Serialização e desserialização em Java: explicando a vulnerabilidade de desserialização do Java.

Voltando ao universo JavaScript, a serialização também é bastante comum.

É provável que você não escreva sua própria lógica de serialização e desserialização. Mas, no maravilhoso mundo do npm, com mais de 1.500.000 pacotes de código aberto à sua disposição, por que não usá-los?

O js-yaml é muito popular, com mais de 28.000.000 de downloads por semana, e tem uma boa avaliação geral de integridade do pacote no Snyk Advisor:

Painel do Snyk Advisor mostrando o pacote npm js-yaml com uma pontuação de integridade de 88/100 e métricas de popularidade, manutenção, segurança e comunidade.

Dito isso, a captura de tela acima do pacote npm js-yaml mostra que versões anteriores tinham vulnerabilidades de segurança. Qual delas, você pergunta?

Foi constatado que algumas versões do js-yaml eram vulneráveis à execução de código devido à desserialização. A vulnerabilidade ocorre por causa do uso a seguir do construtor new Function():

function resolveJavascriptFunction(object /*, explicit*/) {
  /*jslint evil:true*/  var func;

  try {
    func = new Function('return ' + object);
    return func();
  } catch (error) {
    return NIL;
  }
}

Veja como seria uma prova de conceito de exploração dessa vulnerabilidade:

var yaml = require('js-yaml');

x = "test: !!js/function > \n \
function f() { \n \
console.log(1); \n \
}();"

yaml.load(x);

Se um agente mal-intencionado conseguir fornecer uma entrada como essa, ou parte dela, usada para criar a variável x no código de prova de conceito acima, uma possível vulnerabilidade poderá se tornar um risco real.

A vulnerabilidade acima remonta a 2013, mas um relatório de segurança de 2019 identificou outro caso de execução arbitrária de código no js-yaml. Portanto, tenha cuidado. Ou, para dar um conselho mais prático e aplicável: evite new Function() e analise seus pacotes de código aberto de terceiros para garantir que você não tenha essas vulnerabilidades e, caso tenha, possa corrigi-las automaticamente com um pull request de correção.

4. Use um linter de segurança para Node.js

Chegando à parte de ferramentas deste guia, vamos falar de linters. Desenvolvedores JavaScript gostam de usar linters. Seja você usuário do standardjs ou do eslint para impor um estilo de código, essas ferramentas são muito comuns em projetos JavaScript e Node.js.

Por que não impor boas práticas de segurança também? É aí que entra o eslint-plugin-security. Seguir as instruções do README para usar o plugin é bem simples. Basta adicionar a configuração do plugin eslint a seguir para ativar a configuração recomendada:

"plugins": [
  "security"
],
"extends": [
  "plugin:security/recommended"

Como o linter ajuda?

Ele tem regras para detectar práticas de programação inseguras, como detect-eval-with-expression, que identifica usos de eval() com expressões ou literais de string. O linter também tem outras regras, como a de uso das APIs child_process do Node.js.

Vale lembrar que a última publicação do eslint-plugin-security foi há mais de 4 anos. Embora ainda possa funcionar bem, você pode considerar pacotes sucessores, como eslint-plugin-security-node.

5. Use uma ferramenta de análise estática de código para encontrar e corrigir problemas de injeção de código

Linters de análise estática de código (SCA), em suas formas mais básicas, como os usados com o ESLint, são um bom ponto de partida. Eles oferecem contexto suficiente para impor um estilo de código, mas, como vimos com o linter de segurança para Node.js, não são flexíveis o bastante para lidar de fato com problemas de segurança.

Algumas preocupações de desenvolvedores com um linter de segurança para Node.js, como o eslint-plugin-security, são:

  1. Falsos positivos: As regras do linter são bastante básicas e podem gerar muitos falsos positivos, aumentando a frustração e a confusão dos desenvolvedores. Por exemplo, a expressão RegExp(matchEmailRegEx) a seguir fará o linter de segurança para Node.js gerar um erro porque o uso da função RegExp não é literal. Mas talvez matchEmailRegEx seja apenas uma constante no meu arquivo shared/variables.js. O linter não é avançado o suficiente para saber disso.

  2. Regras rígidas demais: Dando continuidade ao ponto anterior, a regra é rígida demais. Ou você usa child_process.exec(someCommand, []), ou não usa. O processo de análise estática de código com o linter não é inteligente o suficiente para identificar que someCommand é uma constante definida diretamente no código. O simples fato de usar child_process.exec() com um valor não literal já gera um erro no linter, frustrando os desenvolvedores, que acabam desativando a regra.

  3. Regras básicas demais: O conjunto de regras é pequeno, e os problemas identificados são muito básicos. Na prática, é tudo ou nada, sem muito contexto sobre como os dados fluem de uma entrada específica do usuário até um código potencialmente sensível, como a execução de comandos, consultas SQL ou outros.

Reforçando o que eu disse antes: um linter de segurança, como o eslint-plugin-security-node ou outros, é um bom ponto de partida. Com certeza é melhor do que não usar nada.

Mas há maneiras melhores de encontrar problemas de segurança no seu próprio código enquanto você programa.

Quero apresentar a você o Snyk Code, uma ferramenta de teste estático de segurança de aplicações (SAST) criada para desenvolvedores.

Encontrando uma injeção de comandos em um aplicativo Node.js

O Snyk Code será lançado em breve, mas vou mostrar uma prévia de como ele funciona.

Primeiro, conecte-se à Snyk com uma conta do GitHub e importe o repositório do GitHub. Para isso, clique em Add project e depois no ícone do GitHub:

Painel de projetos exibindo o menu Adicionar projeto, com o GitHub destacado entre as integrações de repositório disponíveis.

Em seguida, encontre um repositório na lista ou use a caixa de pesquisa para localizá-lo e ative-o para iniciar a análise:

Tela de seleção de repositórios do GitHub filtrada por “goof”, com o repositório “goof” selecionado.

O Snyk importará o repositório do GitHub e fará uma análise rápida.

Ele também detectará automaticamente outros arquivos de manifesto relacionados a possíveis problemas de segurança, como dependências de código aberto com vulnerabilidades conhecidas ou uma imagem Docker que introduza várias vulnerabilidades de segurança.

Vamos nos concentrar no código desta aplicação Node.js. Clique em Code analysis e veja o que encontramos:

Painel de análise de código do repositório lirantal/goof mostrando a quantidade de problemas na análise de código, no Dockerfile e no package.json.

O Snyk Code encontrou várias vulnerabilidades. Uma delas é uma vulnerabilidade de injeção de comandos, como vemos aqui:

Painel de análise de código que destaca uma vulnerabilidade de injeção de comandos em código JavaScript, mostrando a chamada exec vulnerável e detalhes de como corrigir o problema.

Os problemas de segurança encontrados nesta linha de código explicam o risco:

“Unsanitized input from the HTTP request body flows into child_process.exec, where it is used to build a shell command. This may result in a Command Injection vulnerability.”

Mas como os dados fluem do parâmetro url para a função insegura exec()? Clique no botão detalhes completos para ver uma versão mais detalhada do fluxo de dados e entender melhor o contexto:

Análise do fluxo de dados de injeção de comandos, mostrando uma entrada não sanitizada de uma solicitação HTTP chegando a uma chamada exec destacada em routes/index.js.

Aqui, podemos ver claramente o panorama completo da análise feita pelo Snyk Code.

O parâmetro url é criado a partir do array item, que, por sua vez, recebe dados controlados pelo usuário. Esses dados chegam como entrada no corpo da mensagem, na variável req.body.content.

Corrigindo a injeção de comandos

Agora, podemos tomar outras medidas para resolver o problema de segurança, como:

  • Em vez de usar a função insegura exec(), podemos usar a versão segura dessa API: execFile(), que escapa os argumentos fornecidos na forma de um argumento de função do tipo array.

  • Podemos — e devemos — validar, escapar ou higienizar a variável item recebida do usuário antes que ela chegue a um código sensível, como o que executa processos do sistema.

Resumo

Parabéns por ter chegado até aqui!

Esperamos que agora você entenda melhor os problemas que as vulnerabilidades de injeção de código podem causar, sejam elas originadas no seu próprio código ou em dependências de terceiros importadas para a aplicação.

Se esta publicação foi útil, confira também estas leituras recomendadas pelos meus colegas da Snyk:

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.