Skip to content

Boas Práticas de Segurança

Intenção

Este documento pretende expandir o atual modelo de ameaças e fornecer diretrizes abrangentes sobre como proteger um aplicativo Node.js.

Conteúdo do Documento

  • Boas práticas: Uma maneira simplificada e condensada de visualizar as melhores práticas. Podemos usar esta issue ou esta guideline como ponto de partida. É importante notar que este documento é específico para Node.js; se você estiver procurando algo mais amplo, considere as Melhores Práticas OSSF.
  • Ataques explicados: ilustrar e documentar em português claro, com alguns exemplos de código (se possível), os ataques que estamos mencionando no modelo de ameaças.
  • Bibliotecas de Terceiros: definir ameaças (ataques de typosquatting, pacotes maliciosos...) e melhores práticas em relação às dependências de módulos node, etc...

Lista de Ameaças

Negação de Serviço do servidor HTTP (CWE-400)

Este é um ataque em que o aplicativo fica indisponível para a finalidade a que se destina devido à maneira como ele processa solicitações HTTP recebidas. Essas solicitações não precisam ser deliberadamente elaboradas por um ator malicioso: um cliente mal configurado ou com bugs também pode enviar um padrão de solicitações ao servidor que resultam em uma negação de serviço.

As solicitações HTTP são recebidas pelo servidor HTTP Node.js e repassadas ao código do aplicativo por meio do manipulador de solicitações registrado. O servidor não analisa o conteúdo do corpo da solicitação. Portanto, qualquer DoS causado pelo conteúdo do corpo depois que eles são repassados ao manipulador de solicitações não é uma vulnerabilidade no próprio Node.js, pois é responsabilidade do código do aplicativo lidar com isso corretamente.

Certifique-se de que o WebServer lida corretamente com erros de socket; por exemplo, quando um servidor é criado sem um manipulador de erros, ele será vulnerável a DoS.

javascript
import net from 'node:net'
const server = net.createServer(socket => {
  // socket.on('error', console.error) // isso impede o servidor de travar
  socket.write('Echo server\r\n')
  socket.pipe(socket)
})
server.listen(5000, '0.0.0.0')

Se uma solicitação inválida for realizada, o servidor poderá travar.

Um exemplo de ataque DoS que não é causado pelo conteúdo da solicitação é o Slowloris. Neste ataque, as solicitações HTTP são enviadas lentamente e fragmentadas, um fragmento de cada vez. Até que a solicitação completa seja entregue, o servidor manterá os recursos dedicados à solicitação em andamento. Se um número suficiente dessas solicitações for enviado ao mesmo tempo, a quantidade de conexões simultâneas logo atingirá seu máximo, resultando em uma negação de serviço. É assim que o ataque depende não do conteúdo da solicitação, mas do tempo e do padrão das solicitações enviadas ao servidor.

Mitigações

  • Utilize um proxy reverso para receber e encaminhar solicitações para o aplicativo Node.js. Os proxies reversos podem fornecer cache, balanceamento de carga, lista negra de IP, etc., o que reduz a probabilidade de um ataque DoS ser eficaz.
  • Configure corretamente os tempos limite do servidor, para que as conexões que estão inativas ou onde as solicitações estão chegando muito lentamente possam ser descartadas. Consulte os diferentes tempos limite em http.Server, particularmente headersTimeout, requestTimeout, timeout e keepAliveTimeout.
  • Limite o número de sockets abertos por host e no total. Consulte a documentação http, particularmente agent.maxSockets, agent.maxTotalSockets, agent.maxFreeSockets e server.maxRequestsPerSocket.

Religação DNS (CWE-346)

Este é um ataque que pode atingir aplicativos Node.js executados com o inspetor de depuração habilitado usando a opção --inspect.

Como os sites abertos em um navegador da web podem fazer solicitações WebSocket e HTTP, eles podem atingir o inspetor de depuração em execução localmente. Isso geralmente é evitado pela política de mesma origem implementada por navegadores modernos, que proíbe scripts de acessar recursos de origens diferentes (ou seja, um site malicioso não pode ler dados solicitados de um endereço IP local).

No entanto, por meio da religação DNS, um atacante pode controlar temporariamente a origem de suas solicitações para que pareçam originar-se de um endereço IP local. Isso é feito controlando um site e o servidor DNS usado para resolver seu endereço IP. Consulte a wiki de Religação DNS para obter mais detalhes.

Mitigações

  • Desative o inspetor no sinal SIGUSR1 anexando um ouvinte process.on(‘SIGUSR1’, …) a ele.
  • Não execute o protocolo do inspetor na produção.

Exposição de Informações Sensíveis a um Ator Não Autorizado (CWE-552)

Todos os arquivos e pastas incluídos no diretório atual são enviados para o registro npm durante a publicação do pacote.

Existem alguns mecanismos para controlar esse comportamento definindo uma lista de bloqueio com .npmignore e .gitignore ou definindo uma lista de permissão em package.json.

Mitigações

  • Usar npm publish --dry-run para listar todos os arquivos a serem publicados. Certifique-se de revisar o conteúdo antes de publicar o pacote.
  • Também é importante criar e manter arquivos de ignorância, como .gitignore e .npmignore. Nesses arquivos, você pode especificar quais arquivos/pastas não devem ser publicados. A propriedade files em package.json permite a operação inversa de lista -- permitida.
  • Em caso de exposição, certifique-se de despublicar o pacote.

Contrabandeo de Solicitação HTTP (CWE-444)

Este é um ataque que envolve dois servidores HTTP (normalmente um proxy e um aplicativo Node.js). Um cliente envia uma solicitação HTTP que passa primeiro pelo servidor front-end (o proxy) e depois é redirecionado para o servidor back-end (o aplicativo). Quando o front-end e o back-end interpretam solicitações HTTP ambíguas de forma diferente, existe o potencial de um atacante enviar uma mensagem maliciosa que não será vista pelo front-end, mas será vista pelo back-end, efetivamente "contrabandeando-a" além do servidor proxy.

Consulte CWE-444 para obter uma descrição e exemplos mais detalhados.

Como esse ataque depende da interpretação de solicitações HTTP pelo Node.js de forma diferente de um servidor HTTP (arbitrário), um ataque bem-sucedido pode ser devido a uma vulnerabilidade no Node.js, no servidor front-end ou em ambos. Se a maneira como a solicitação é interpretada pelo Node.js é consistente com a especificação HTTP (consulte RFC7230), então não é considerada uma vulnerabilidade no Node.js.

Mitigações

  • Não use a opção insecureHTTPParser ao criar um servidor HTTP.
  • Configure o servidor front-end para normalizar solicitações ambíguas.
  • Monitore continuamente as novas vulnerabilidades de contrabando de solicitações HTTP no Node.js e no servidor front-end escolhido.
  • Use HTTP/2 de ponta a ponta e desative a degradação HTTP se possível.

Exposição de Informações por meio de Ataques de Temporização (CWE-208)

Este é um ataque que permite ao atacante aprender informações potencialmente sensíveis, por exemplo, medindo o tempo que o aplicativo leva para responder a uma solicitação. Este ataque não é específico do Node.js e pode atingir quase todos os tempos de execução.

O ataque é possível sempre que o aplicativo usa um segredo em uma operação sensível ao tempo (por exemplo, ramificação). Considere o tratamento de autenticação em um aplicativo típico. Aqui, um método de autenticação básico inclui e-mail e senha como credenciais. As informações do usuário são recuperadas da entrada fornecida pelo usuário, idealmente de um DBMS. Após recuperar as informações do usuário, a senha é comparada com as informações do usuário recuperadas do banco de dados. Usar a comparação de strings integrada leva mais tempo para valores de mesmo comprimento. Essa comparação, quando executada por uma quantidade aceitável, aumenta involuntariamente o tempo de resposta da solicitação. Comparando os tempos de resposta da solicitação, um atacante pode adivinhar o comprimento e o valor da senha em uma grande quantidade de solicitações.

Mitigações

  • A API criptográfica expõe uma função timingSafeEqual para comparar valores sensíveis reais e esperados usando um algoritmo de tempo constante.
  • Para comparação de senhas, você pode usar o scrypt disponível também no módulo criptográfico nativo.
  • De forma mais geral, evite usar segredos em operações de tempo variável. Isso inclui ramificar segredos e, quando o atacante puder estar co-localizado na mesma infraestrutura (por exemplo, mesma máquina na nuvem), usar um segredo como índice na memória. Escrever código de tempo constante em JavaScript é difícil (em parte por causa do JIT). Para aplicativos criptográficos, use as APIs criptográficas integradas ou WebAssembly (para algoritmos não implementados nativamente).

Módulos de Terceiros Maliciosos (CWE-1357)

Atualmente, no Node.js, qualquer pacote pode acessar recursos poderosos, como acesso à rede. Além disso, como eles também têm acesso ao sistema de arquivos, eles podem enviar quaisquer dados para qualquer lugar.

Todo o código executado em um processo do nó tem a capacidade de carregar e executar código arbitrário adicional usando eval() (ou seus equivalentes). Todo o código com acesso de gravação no sistema de arquivos pode alcançar o mesmo resultado gravando em arquivos novos ou existentes que são carregados.

O Node.js possui um mecanismo de política experimental¹ para declarar o recurso carregado como não confiável ou confiável. No entanto, essa política não está habilitada por padrão. Certifique-se de fixar as versões de dependência e executar verificações automáticas de vulnerabilidades usando fluxos de trabalho comuns ou scripts npm. Antes de instalar um pacote, certifique-se de que este pacote seja mantido e inclua todo o conteúdo que você esperava. Cuidado, o código-fonte do GitHub nem sempre é o mesmo que o publicado, valide-o no node_modules.

Ataques à cadeia de suprimentos

Um ataque à cadeia de suprimentos em um aplicativo Node.js acontece quando uma de suas dependências (seja direta ou transitiva) é comprometida. Isso pode acontecer devido ao aplicativo ser muito flexível na especificação das dependências (permitindo atualizações indesejadas) e/ou erros de digitação comuns na especificação (vulnerável a typosquatting).

Um atacante que assume o controle de um pacote upstream pode publicar uma nova versão com código malicioso nela. Se um aplicativo Node.js depender desse pacote sem ser estrito sobre qual versão é segura para usar, o pacote pode ser atualizado automaticamente para a versão maliciosa mais recente, comprometendo o aplicativo.

As dependências especificadas no arquivo package.json podem ter um número de versão exato ou uma faixa. No entanto, ao fixar uma dependência em uma versão exata, suas dependências transitivas não são fixadas. Isso ainda deixa o aplicativo vulnerável a atualizações indesejadas/inesperadas.

Possíveis vetores de ataque:

  • Ataques de typosquatting
  • Envenenamento de lockfile
  • Mantenedores comprometidos
  • Pacotes maliciosos
  • Confusões de dependências
Mitigações
  • Impeça o npm de executar scripts arbitrários com --ignore-scripts
    • Além disso, você pode desabilitá-lo globalmente com npm config set ignore-scripts true
  • Fixe as versões das dependências em uma versão específica imutável, não em uma versão que seja uma faixa ou de uma fonte mutável.
  • Use lockfiles, que fixam todas as dependências (diretas e transitivas).
  • Automatize as verificações de novas vulnerabilidades usando CI, com ferramentas como npm-audit.
    • Ferramentas como Socket podem ser usadas para analisar pacotes com análise estática para encontrar comportamentos arriscados, como acesso à rede ou ao sistema de arquivos.
  • Use npm ci em vez de npm install. Isso reforça o lockfile, de modo que inconsistências entre ele e o arquivo package.json causem um erro (em vez de ignorar silenciosamente o lockfile em favor de package.json).
  • Verifique cuidadosamente o arquivo package.json para erros/erros de digitação nos nomes das dependências.

Violação de Acesso à Memória (CWE-284)

Ataques baseados em memória ou heap dependem de uma combinação de erros de gerenciamento de memória e um alocador de memória explorável. Como todas as runtimes, o Node.js é vulnerável a esses ataques se seus projetos rodarem em uma máquina compartilhada. Usar um heap seguro é útil para evitar vazamento de informações sensíveis devido a estouros e subornos de ponteiros.

Infelizmente, um heap seguro não está disponível no Windows. Mais informações podem ser encontradas na documentação do Node.js sobre heap seguro.

Mitigações

  • Use --secure-heap=n dependendo do seu aplicativo, onde n é o tamanho máximo de bytes alocado.
  • Não execute seu aplicativo de produção em uma máquina compartilhada.

Monkey Patching (CWE-349)

Monkey patching refere-se à modificação de propriedades em tempo de execução com o objetivo de alterar o comportamento existente. Exemplo:

js
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // sobrescrevendo o global [].push
}

Mitigações

O flag --frozen-intrinsics habilita intrínsecos congelados experimentais¹, o que significa que todos os objetos e funções JavaScript embutidos são congelados recursivamente. Portanto, o trecho a seguir não substituirá o comportamento padrão de Array.prototype.push

js
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // sobrescrevendo o global [].push
}
// Uncaught:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Cannot assign to read only property 'push' of object '

No entanto, é importante mencionar que você ainda pode definir novos globais e substituir globais existentes usando globalThis

bash
globalThis.foo = 3; foo; // você ainda pode definir novos globais 3
globalThis.Array = 4; Array; // No entanto, você também pode substituir globais existentes 4

Portanto, Object.freeze(globalThis) pode ser usado para garantir que nenhum global será substituído.

Ataques de Poluição de Protótipo (CWE-1321)

Poluição de protótipo refere-se à possibilidade de modificar ou injetar propriedades em itens de linguagem Javascript abusando do uso de __proto__, _constructor, prototype e outras propriedades herdadas de protótipos embutidos.

js
const a = { a: 1, b: 2 }
const data = JSON.parse('{"__proto__": { "polluted": true}}')
const c = Object.assign({}, a, data)
console.log(c.polluted) // true
// Potencial DoS
const data2 = JSON.parse('{"__proto__": null}')
const d = Object.assign(a, data2)
d.hasOwnProperty('b') // Uncaught TypeError: d.hasOwnProperty is not a function

Esta é uma vulnerabilidade potencial herdada da linguagem JavaScript.

Exemplos

Mitigações

  • Evite mesclas recursivas inseguras, veja CVE-2018-16487.
  • Implemente validações de Schema JSON para solicitações externas/não confiáveis.
  • Crie Objetos sem protótipo usando Object.create(null).
  • Congelando o protótipo: Object.freeze(MyObject.prototype).
  • Desative a propriedade Object.prototype.__proto__ usando a flag --disable-proto.
  • Verifique se a propriedade existe diretamente no objeto, não no protótipo usando Object.hasOwn(obj, keyFromObj).
  • Evite usar métodos de Object.prototype.

Elemento de Caminho de Busca Não Controlado (CWE-427)

O Node.js carrega módulos seguindo o Algoritmo de Resolução de Módulos. Portanto, ele presume que o diretório em que um módulo é solicitado (require) é confiável.

Com isso, significa que o seguinte comportamento do aplicativo é esperado. Assumindo a seguinte estrutura de diretório:

  • app/
    • server.js
    • auth.js
    • auth

Se server.js usa require('./auth'), ele seguirá o algoritmo de resolução de módulo e carregará auth em vez de auth.js.

Mitigações

Usar o mecanismo de política com verificação de integridade experimental¹ pode evitar a ameaça acima. Para o diretório descrito acima, pode-se usar o seguinte policy.json

json
{
  "resources": {
    "./app/auth.js": {
      "integrity": "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8="
    },
    "./app/server.js": {
      "dependencies": {
        "./auth": "./app/auth.js"
      },
      "integrity": "sha256-NPtLCQ0ntPPWgfVEgX46ryTNpdvTWdQPoZO3kHo0bKI="
    }
  }
}

Portanto, ao requisitar o módulo auth, o sistema validará a integridade e lançará um erro se não corresponder ao esperado.

bash
» node --experimental-policy=policy.json app/server.js
node:internal/policy/sri:65
      throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
      ^
SyntaxError [ERR_SRI_PARSE]: Subresource Integrity string "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8=%" had an unexpected "%" at position 51
    at new NodeError (node:internal/errors:393:5)
    at Object.parse (node:internal/policy/sri:65:13)
    at processEntry (node:internal/policy/manifest:581:38)
    at Manifest.assertIntegrity (node:internal/policy/manifest:588:32)
    at Module._compile (node:internal/modules/cjs/loader:1119:21)
    at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
    at Module.load (node:internal/modules/cjs/loader:1037:32)
    at Module._load (node:internal/modules/cjs/loader:878:12)
    at Module.require (node:internal/modules/cjs/loader:1061:19)
    at require (node:internal/modules/cjs/helpers:99:18) {
  code: 'ERR_SRI_PARSE'
}

Observação, é sempre recomendado o uso de --policy-integrity para evitar mutações de política.

Recursos Experimentais em Produção

O uso de recursos experimentais em produção não é recomendado. Recursos experimentais podem sofrer alterações drásticas se necessário, e sua funcionalidade não é seguramente estável. Apesar disso, feedback é muito apreciado.

Ferramentas OpenSSF

O OpenSSF lidera diversas iniciativas que podem ser muito úteis, especialmente se você planeja publicar um pacote npm. Essas iniciativas incluem:

  • Scorecard OpenSSF O Scorecard avalia projetos de código aberto usando uma série de verificações de risco de segurança automatizadas. Você pode usá-lo para avaliar proativamente vulnerabilidades e dependências em sua base de código e tomar decisões informadas sobre a aceitação de vulnerabilidades.
  • Programa de Insígnias de Boas Práticas OpenSSF Projetos podem voluntariamente se autocertificar descrevendo como cumprem cada boa prática. Isso gerará uma insígnia que pode ser adicionada ao projeto.