Como derrubar um servidor de e-mail com uma única mensagem
Joran Greef
1 de agosto de 2018
0 minutos de leituraCinco dos parsers de e-mail mais populares para Node.js foram recentemente identificados como vulneráveis a um ataque trivial de negação de serviço (DoS). A vulnerabilidade pode ser explorada ao incluir alguns milhões de anexos vazios em um e-mail, contornando os limites de tamanho típicos (geralmente de 20 MB ou menos). Quando o e-mail é enviado a um servidor vulnerável, ele congela o event loop do Node.js por vários segundos devido ao enorme número de anexos. O uso de memória dispara para 2 GB ou mais por causa dos objetos internos criados para cada anexo — o que normalmente basta para derrubar o servidor inteiro com uma falha por falta de memória. Então, seu servidor Node.js processa e-mails? Você sabe qual parser de e-mail está usando? Antes de conferir, vamos ver quem é afetado.
Antes de continuar, aqui está o XKCD obrigatório.

Um ataque de negação de serviço não deveria ser tão fácil, certo?
A vulnerabilidade é fácil de explicar, fácil de explorar e afeta milhares de sistemas. A biblioteca mailparser, por exemplo, recebe até 249.400 downloads mensais e é usada como dependência por outros 214 projetos, incluindo o Sendgrid. O Haraka é outra biblioteca afetada, usada por Craigslist, Fort Anti-Spam e ThreatWave.
A correção é uma linha de código. Você não precisa do Cloudflare. Basta validar os dados do usuário. Isso pode ser feito contando o número de anexos (incluindo partes de texto) e reagindo quando a contagem passar de 1.000, por exemplo. Quando foi a última vez que você viu um e-mail com 10.000 anexos? Quando precisou enviar 100.000 anexos? E, se você ainda está processando depois de um milhão de anexos… bom, sabe que foi longe demais!
Espera aí, como deixamos isso passar? Se é tão fácil corrigir quanto afirmamos, como pelo menos cinco implementações erraram? E como encontramos a vulnerabilidade — e o que podemos fazer para melhorar?
Imagine que você está escrevendo um parser de e-mail…
Você sabe quantas RFCs precisa ler (e interpretar). Sabe quantos testes precisa escrever para garantir que esteja em conformidade com as RFCs o máximo possível. Já ouviu o mantra do desenvolvimento de software: “faça funcionar e depois deixe rápido”. Mas parsers de e-mail são difíceis, e você acaba ficando feliz se simplesmente funcionarem. Depois de escrever um, dá para entender por que talvez você não faça um cálculo de guardanapo rápido para estimar quanta memória um único objeto multipart do seu projeto pode alocar. Se você fizer uma análise de complexidade:
É provável que você meça apenas o uso de CPU, não de memória.Provavelmente, você não vai medir o consumo de memória típico.Você acaba não considerando nenhum ambiente SMTP específico e, por isso, não tenta acelerar caminhos comuns no parser, embora 90% dos e-mails processados sejam spam.Você evita impor muitas políticas rígidas ao parser.Seus usuários talvez não gostem de um limite para o número máximo de anexos por e-mail.Você prefere processar tudo e deixar para o usuário rejeitar a mensagem durante a transação SMTP, se necessário. Afinal, você não é o administrador do servidor de e-mail.
Imagine que você administra um servidor de e-mail…
Uma das primeiras coisas que você provavelmente fará é definir o limite de tamanho dos e-mails. Quanto maior o e-mail, menor a chance de outros servidores aceitá-lo. Você define o limite em apenas 20 MB, pensando que isso também manterá o tempo de processamento dentro de parâmetros razoáveis. Decide usar um parser de e-mail popular e testado em batalha. Confia que a complexidade do parser é linear, ou O(N), em relação ao tamanho do e-mail. Mede o uso de CPU com e-mails de até 20 MB e espera que o servidor processe milhares de mensagens por segundo. Espera que todos os e-mails de 20 MB levem aproximadamente o mesmo tempo. Calcula que apenas 8 GB de RAM devem bastar para 200 e-mails simultâneos de 20 MB por segundo, pois não espera que sua base de usuários cresça tão rápido. Se alguém apostasse com você se o servidor aguenta 10 e-mails simultâneos de 20 MB, você teria confiança. A última coisa que passaria pela sua cabeça seria um anexo de 0 byte. Que mal um arquivo vazio poderia fazer?
E então você vê isto:
Como encontramos a vulnerabilidade?
A Ronomon é uma startup de e-mail em beta privado. Quando comecei, tive uma experiência terrível: mensagens recebidas faziam o coletor de lixo do V8 bloquear o event loop do Node.js por dezenas de segundos seguidos. Passei dois dias das minhas férias alternando sinalizadores de recursos a cada poucos minutos para reduzir a carga, enquanto tentava entender o que estava acontecendo. Por fim, com a ajuda de Vyacheslav Egorov, comentei o funcionamento interno da função CollectAllAvailableGarbage do V8, que estava realizando, sem problemas, sete ciclos de coleta arbitrários em um heap gigantesco, de vários gigabytes. Olhando para trás, foi uma ótima experiência de aprendizado. Passei a ter muito cuidado ao alocar objetos no heap e ao bloquear o event loop.
No início do ano passado, comecei a escrever um novo parser de e-mail, que desde então foi disponibilizado como código aberto como @ronomon/mime. O objetivo era torná-lo 10Ã mais rápido que o parser anterior, com o mínimo de alocações, operando em buffers brutos, em conformidade com as RFCs e com 100% de cobertura de testes, incluindo testes fuzz. Foi difícil alcançar esse objetivo e precisei fazer coisas como eliminar conversões de buffer para string e reduzir previsões incorretas de desvios causadas por linhas de Base64 quebradas a cada 78 caracteres.
Ao longo do caminho, aprendi que algumas decisões de política talvez sejam mais bem tomadas no parser de e-mail, e outras, no servidor — ou vice-versa. Isso incluiu decisões como rejeitar dados Base64, Quoted-Printable ou codificações de caracteres claramente maliciosos, corrompidos ou truncados; rejeitar cabeçalhos críticos duplicados; limitar o número de partes multipart; e limitar o retrocesso causado por limites multipart identificados incorretamente.
No início deste ano, entrei em contato com Jamie Davis, autor de um excelente guia sobre como evitar o bloqueio do event loop do Node.js. Jamie estava fazendo pesquisas sobre ataques ao event loop, e sugeri que alguns parsers de e-mail disponíveis poderiam ser vulneráveis a um ataque multipart. Fiquei surpreso ao ver a hipótese confirmada em todos os parsers de e-mail para Node.js que testei.
Dez coisas que podemos fazer melhor
Veja dez dicas e boas práticas que vale a pena considerar:
Ataques DoS exploram a escassez de recursos. Demonstre empatia mecânica e lembre-se de que você está escrevendo para uma máquina. Valorize os recursos mecânicos que foram confiados a você. Use-os com responsabilidade. Não desperdice. Não pense apenas em O(N) para o uso de CPU: considere também O(N) nas alocações de memória e em todos os recursos que você utiliza. Se seu código for eficiente em CPU, memória, disco e rede, será menos vulnerável ao esgotamento de recursos e você terá mais chances de definir limites razoáveis para todos os recursos do sistema.
Faça cálculos de guardanapo considerando todas as dimensões de recursos desde o início de qualquer projeto. Isso ajuda a identificar projetos mal planejados mais cedo e evita que você tente implementar o “impossível”. Desempenho e segurança não são coisas que dá para otimizar ou adicionar depois. Precisam ser considerados desde o começo. Não espere seu módulo se tornar popular.
Equilibre o uso de recursos em todas as dimensões. Você pode ter CPU suficiente para atingir suas metas de processamento, mas ficará sem memória antes disso? Mais uma vez, faça cálculos de guardanapo para manter o uso proporcional entre as dimensões de recursos e evitar gargalos no projeto.
Lembre-se: um problema de desempenho é apenas um DoS esperando para acontecer — especialmente quando você está usando um event loop. Da próxima vez que um usuário relatar um problema de desempenho, encare-o como uma oportunidade de evitar um problema de segurança.
Valide todos os dados do usuário: não apenas “quanto”, mas também “quantos”. Na verdade, muitas vezes são as coisas pequenas que exigem mais atenção, porque ocorrem com mais frequência e podem ser multiplicadas e amplificadas contra você.
Pergunte-se sempre: o que considero realista? Não permita nada 10Ã além do seu limite realista. Incorpore suas expectativas ao código que escreve.
Fique atento às lacunas entre os limites dos módulos. Não presuma que “outra pessoa vai cuidar disso”. Não deixe decisões de política sem responsável. Talvez seja necessário entender melhor suas dependências.
Trate dados claramente inválidos como tóxicos. Não chegue nem perto. Descarte-os assim que puder.
Pergunte-se: o que um usuário mal-intencionado faria? Não se limite a revisar seu código. Tente explorá-lo ativamente. Pense em pelo menos três formas de exploração em cada módulo e corrija-as antes de publicar. Defina uma meta e encontre essas falhas. Elas sempre estão lá. Você vai se surpreender.
Os testes fuzz têm uma imaginação incrível. Escreva seus próprios testes fuzz simples para gerar uma variedade aleatória de argumentos válidos e inválidos. Compare os valores de retorno da sua função com outra implementação para verificar a correção nos casos válidos e as exceções nos inválidos. Testes fuzz que executam milhões de combinações de argumentos são como a Lei de Linus levada ao extremo: simulam a capacidade de milhares de olhares de encontrar bugs em apenas alguns segundos.
Cronograma de divulgação privada e pública
A vulnerabilidade foi comunicada em privado aos responsáveis pelos módulos afetados em 23 de abril de 2018. Alguns dias antes do prazo de 90 dias para a divulgação pública, os responsáveis tiveram a oportunidade de adiá-la por qualquer motivo. Além disso, os responsáveis pelos módulos dependentes mais ativos foram contatados (quando havia dados de contato disponíveis no GitHub) e preparados para a divulgação pública em 25 de junho de 2018.
Agradecemos a Karen Yavine, Simon Maple e Danny Grander, da Snyk, pela ajuda na divulgação pública, pela sugestão deste artigo e pela investigação adicional. Agradecemos também, em especial, a Matt Sergeant, do Haraka, pela resposta rápida.
haraka
https://snyk.io/vuln/npm:haraka:20180625
mailparser
https://snyk.io/vuln/npm:mailparser:20180625
emailjs-mime-parser
https://snyk.io/vuln/npm:emailjs-mime-parser:20180625
mailsplit
https://snyk.io/vuln/npm:mailsplit:20180625
mailparser-mit
https://snyk.io/vuln/npm:mailparser-mit:20180625
Comece a participar de desafios de Capture the Flag
Aprenda a resolver desafios de Capture the Flag assistindo sob demanda ao nosso workshop virtual introdutório.
