Vulnerabilidade de não validação de certificado HTTPS em Node.js
12 de agosto de 2021Ontem – 11 de agosto de 2021 – o Node.js anunciou e lançou uma correção de segurança para CVE-2021-22939 , junto com dois outros problemas de alta gravidade.
Eles classificaram essa vulnerabilidade como ‘baixa gravidade’, mas acho que vale a pena dar uma olhada mais de perto, pois (imo) isso realmente subestima o risco aqui e o impacto potencialmente generalizado.
Por Tim Perry
Na prática, isso representa um risco para qualquer pessoa que faça conexões TLS a partir do Node.js, por exemplo, qualquer pessoa que faça solicitações HTTPS. Nem todo uso é vulnerável, mas muitos casos de uso comuns são; não é fácil garantir que seu código seja 100% seguro e todas as versões do Node.js, desde pelo menos a v8.0.0, são afetadas. Se estiver usando TLS / HTTPS em Node.js, você deve atualizar o mais rápido possível .
Eu mesmo relatei esse problema ao Node algumas semanas atrás, depois de encontrá-lo durante meu próprio kit de ferramentas HTTP de teste de desenvolvimento .
Vamos explicar por que isso é um problema, como funciona e o que você deve fazer a respeito.
Tudo aqui se aplica ao TLS em geral, mas vou me concentrar no HTTPS especificamente, já que é de longe o caso de uso mais provável e é mais simples e claro.
Qual é o problema?
Aqui está um exemplo de código comum, mas vulnerável (tipos TypeScript incluídos para maior clareza):
const https = require('https');
// Any convenient wrapper or library around the HTTPS module. It takes a URL, and
// extra optional parameters, including a `verifyCertificates` option, which can
// be set to `false` to disable cert verification when necessary.
function makeRequest(url: string, options: { verifyCertificates?: boolean } = {}) {
// [...Do some custom logic...]
// At some point make a request, using the optional verification option:
return https.get(url, {
rejectUnauthorized: options.verifyCertificates
});
}
// Later usage looks like it's making a secure HTTPS request, but in fact the certificate
// is not being verified at all, so you could be talking to *anybody*:
makeRequest("https://google.com");
A chave aqui é rejectUnauthorized
. Esta opção Node.js configura se a solicitação irá verificar se o certificado do servidor é válido. Se estiver desativado, todas as proteções HTTPS serão desativadas silenciosamente. Qualquer pessoa que puder tocar em seu tráfego HTTPS pode se passar por qualquer servidor para inspecionar ou editar qualquer tráfego de sua preferência. Em termos de segurança, isso é normalmente descrito como “Muito ruim”.
A documentação do Node.js para esta opção diz:
Se não for falso, o certificado do servidor é verificado em relação à lista de CAs fornecidos. […] Padrão: verdadeiro.
Ou seja, você pode desativar ativamente esta verificação se precisar, mas por padrão ela está sempre ativada, a menos que você passe explicitamente false
.
Infelizmente, isso não era verdade.
Na realidade, os valores falsey incluindo undefined
, também desabilitarão a verificação do certificado. Isso é um problema porque é extremamente fácil introduzir valores falsey em JavaScript. Além disso, todas as outras APIs tratam undefined
o mesmo como ‘nenhum parâmetro fornecido’ e, portanto, usam o padrão ( true
), que validaria com segurança o certificado do servidor. É assim que todas as outras APIs de Node.js que testei funcionam e como o suporte sintático para parâmetros padrão é definido.
Isso transforma o código que passa undefined
de algo que a maioria dos desenvolvedores presumiria ser perfeitamente seguro e protegido, e que a documentação diz explicitamente que é seguro, em algo que desabilita de forma invisível as proteções de segurança fundamentais.
Isso significa que qualquer pessoa que passar acidentalmente undefined
para ele rejectUnauthorized
não está verificando o certificado TLS do servidor, e todas as proteções de HTTPS foram desativadas silenciosamente .
Quando a verificação de certificado é desabilitada assim, vale tudo. Você pode usar um certificado autoassinado que acabou de criar, usar um certificado real assinado com o nome de host errado, usar certificados expirados, certificados revogados ou até mesmo muitos certificados inválidos.
Isso permite que qualquer parte mal-intencionada que possa se interpor entre você e o servidor de destino finja ser esse servidor e, assim, leve todo o seu tráfego em ambas as direções e inspecione e / ou modifique o tráfego proxy antes de ele ser enviado para o servidor real.
O Node.js não mostrará nenhum aviso ou pista de que isso está acontecendo e, quando não houver partes maliciosas envolvidas, tudo funcionará exatamente como normal, tornando isso uma vulnerabilidade de outra forma invisível.
Cair nessa undefined
armadilha é fácil, por causa de como as pessoas frequentemente criam objetos de opções como esses em JavaScript. Uma convenção comum é definir todas as propriedades, referenciando opções de outros lugares que podem ou não ser definidas. Isso não é algo que você costuma fazer ao fazer uma solicitação HTTP (S) do zero em seu próprio código, mas é um padrão muito comum ao construir uma biblioteca ou wrapper menor em torno das APIs HTTPS brutas.
Qualquer código como este geralmente é vulnerável:
https.request({
// ...
rejectUnauthorized: options.anOption
});
Isso é vulnerável porque os valores nos options
objetos são opcionais por definição (então geralmente serão indefinidos), enquanto rejectUnauthorized
não se comporta como uma opção normal e nunca deveria ser undefined
.
Isso não precisa ser necessariamente tão simples. É bem possível que as bibliotecas internas gerem valores para com rejectUnauthorized
base em outros parâmetros (permitindo certificados autoassinados para nomes de host específicos, por exemplo), portanto, há uma variedade de maneiras de fazer isso.
Na prática, isso é comum – basta dizer que estou ciente dos módulos npm com milhões de downloads semanais que seguem esse padrão hoje, e há muitos exemplos que você pode encontrar no GitHub também. Vou evitar apontar para detalhes antes que eles sejam totalmente resolvidos, mas pretendo coordenar com pacotes vulneráveis que estou ciente para atualizar este código, e os aplicativos que estão sendo executados em uma das versões mais recentes do Node.js são seguros de qualquer maneira.
Como um invasor pode explorar isso?
Explorar isso é trivial se você conseguir acessar o caminho de rede de uma solicitação de um aplicativo vulnerável. Isso geralmente significa que qualquer pessoa em sua rede local pode explorar isso (por exemplo, no mesmo wi-fi enquanto você usa uma ferramenta CLI Node.js vulnerável) ou qualquer pessoa que lida com seu tráfego upstream, por exemplo, seu ISP, qualquer proxy, serviços de proxy reverso como CloudFlare, e assim por diante.
Embora isso seja um desafio, os invasores no caminho entre você e o servidor com o qual você está falando são exatamente o que o HTTPS está tentando evitar, e a única razão pela qual ele existe. Essas proteções são importantes e a grande maioria do software que você usa presume que elas estão em vigor e constrói outros mecanismos de segurança sobre essa base.
Explorar isso na realidade requer três etapas:
- Esteja no caminho entre um cliente vulnerável e um servidor HTTPS com o qual eles desejam se comunicar (para um ISP ou proxy, isso é sempre verdade, para redes locais isso pode ser obtido de forma confiável usando várias técnicas como ARP spoofing ou wi-fi gêmeo maléfico )
- Finja ser o servidor de destino (aceite a conexão TLS ao vê-la, gere um certificado aleatório para o handshake TLS e o código vulnerável sempre o aceitará como um certificado válido real, independentemente)
- Faça algo com o tráfego interceptado (faça proxy para o servidor real intocado, mas inspecionado, injete suas próprias respostas ou faça proxy enquanto altera os dados de solicitação e resposta)
As etapas 2 e 3 são extremamente fáceis, e bibliotecas como Mockttp (que mantenho, para testar o tráfego de solicitação HTTP (S)) podem fazer isso para você automaticamente em algumas linhas . A etapa 1 é mais difícil, mas não muito mais difícil em muitos ambientes.
Os clientes Node.js em risco provavelmente são ferramentas CLI ou serviços de back-end que fazem solicitações a APIs. Para clientes vulneráveis, tais chaves de API são expostas e todas as solicitações e respostas de API são potencialmente visíveis e editáveis por terceiros durante o trajeto. Para a maioria dos usos não triviais da API, isso é muito ruim.
Qual é a solução?
Para o próprio Node.js, é uma correção muito simples: o módulo TLS precisa verificar explicitamente false
, o que agora é feito . Isso já foi feito para a verificação do servidor de certificados de cliente um tempo atrás (quando a documentação foi atualizada), mas aparentemente nunca foi concluído para a (muito mais comumente usada) verificação de cliente de certificados de servidor.
Para desenvolvedores downstream, há duas coisas que você pode fazer:
- Atualize para a versão mais recente do Node.js
- Certifique-se de que todo o seu código e seu código de dependências sempre sejam definidos
rejectUnauthorized
explicitamente comotrue
(por padrão) oufalse
(somente onde definitivamente necessário).
Para testar se o código é vulnerável, tente fazer uma solicitação a um serviço HTTPS sabidamente inválido. Badssl.com hospeda uma seleção desses, cobrindo vários tipos de configurações HTTPS incorretas, por exemplo expired.badssl.com . Infelizmente, devido à natureza disso, você precisa garantir que funcione com vários
No código do exemplo acima, makeRequest("https://expired.badssl.com")
funcionará, enviando a solicitação sem erros. Usando uma das versões corrigidas do nó lançadas hoje, ou ao consertar o próprio código, ele gerará um erro.
Por que esse é um problema de baixa gravidade?
Boa pergunta! Se você não estiver interessado em como funcionam os relatórios de segurança, isso pode não ser interessante, mas é importante, porque essas pontuações de gravidade afetam a quantidade de atenção que as vulnerabilidades recebem e a rapidez com que os sistemas são protegidos.
Se você não estiver ciente, as vulnerabilidades são geralmente pontuadas usando CVSS (Common Vulnerability Scoring System), que pega uma série de parâmetros como “Impacto de confidencialidade” e “Privilégios necessários” e os combina para fornecer uma gravidade geral de 0,0 (não problema) a 10,0 (desastre crítico). O conjunto de parâmetros em conjunto descreve os detalhes de como a vulnerabilidade é explorada e seu impacto potencial nos sistemas vulneráveis.
Essas pontuações não estão relacionadas a quantos sistemas são afetados, ou a chance de uma pessoa aleatória na rua ser afetada, ou qualquer coisa assim – uma vulnerabilidade Node.js não é pontuada mais alta do que o bug equivalente em um minúsculo raramente usado Pacote FORTRAN. Essas pontuações objetivam apenas dar uma pontuação do risco potencial para os sistemas que são vulneráveis, para que os mantenedores desses sistemas entendam sua exposição.
Na minha opinião, usando as definições padrão de CVSS , uma boa medida da gravidade real é:
- Vetor de ataque: rede (você pode atacar remotamente, se estiver entre o cliente e o servidor de destino)
- Complexidade de ataque: alta (você precisa se posicionar entre o cliente e o servidor de destino)
- Privilégios exigidos: Nenhum (você não precisa de uma conta de usuário no servidor ou aplicativo vulnerável)
- Interação do usuário: Nenhuma (os invasores podem explorar isso sem nenhuma ação do usuário envolvida)
- Escopo: inalterado (geralmente afeta apenas o aplicativo vulnerável)
- Impacto da confidencialidade: alto (um invasor pode inspecionar todo o tráfego HTTPS)
- Impacto na integridade: alto (um invasor pode alterar arbitrariamente qualquer tráfego HTTPS)
- Impacto na disponibilidade: Nenhum (generosamente – existem ataques avançados em que você pode usar isso para tornar um aplicativo vulnerável indisponível, mas a maioria dos ataques não fica)
As definições deles são padronizadas e este é um exemplo clássico de muitos deles (a interceptação do tráfego de rede é literalmente o exemplo de “Complexidade de Ataque: Alta” tirado diretamente do padrão).
Colocar o acima na calculadora classifica isso em 7,4 de 10 (gravidade alta). Isso está de acordo com muitas vulnerabilidades muito semelhantes no passado – por exemplo, em módulos npm , módulos Ruby , módulos Java e WordPress – e é uma representação muito mais clara do risco real aqui, na minha opinião.
(Se há algo que estou perdendo, no entanto, que limita o risco ou aumenta o desafio de explorar isso, adoraria ouvir sobre isso! Entre em contato )
Não tenho certeza de todas as razões pelas quais o Node está tratando isso como um problema de baixa gravidade (1.9), mas suspeito que seja devido a um simples mal-entendido sobre a capacidade de exploração geral, ou a equipe do Node considera a proteção contra undefined
opções como essa para ser descartada de escopo, embora sejam amplamente usados e ativamente apoiados. Eu tenho tentado resolver isso mesmo, mas sem sucesso.
No entanto, devo agradecer a eles: embora eu discorde dessa decisão, eles ainda fizeram uma triagem rápida do relatório, encontraram a causa e enviaram uma correção para o problema.
Linha do tempo de vulnerabilidade
- 26 de julho: Encontro o problema e apresento um relatório.
- 28 de julho: A equipe do Node reconhece o problema e encontra o provável cuplrit.
- 5 de agosto: a equipe do Node anuncia um próximo lançamento de segurança.
- 9 de agosto: Uma correção foi confirmada .
- 11 de agosto: Node.js v16.6.2, v14.17.5 e v12.22.5 são lançados com correções para este problema e outros.
Empacotando
Você pode estar vulnerável a este problema: há uma abundância de código à solta que claramente está, e é muito fácil se tornar vulnerável se você escreveu sua própria função de utilitário de solicitação de HTTP ou semelhante.
Se você estiver vulnerável, isso é potencialmente fácil de explorar e o impacto é muito significativo.
Felizmente, isso é fácil de corrigir. Atualize o Node sempre que puder para v16.6.2, v14.17.5 ou v12.22.5 agora , e atualize qualquer outro código que passe um valor potencialmente indefinido rejectUnauthorized
para garantir que seja um booleano (padronizado como verdadeiro) também, sempre que possível, por precaução.
Tem alguma opinião ou feedback? Entre em contato pelo Twitter ou mande uma mensagem e me avise.
Quer inspecionar e depurar HTTPS Node.js por si mesmo, para depuração e teste, sem vulnerabilidades necessárias?
Fonte: HTTP Toolkit .
Acesse aqui e saiba tudo sobre TLS, o protocolo de segurança que garante o sigilo das informações e identifica empresas, dispositivos e objetos no mundo eletrônico.