Detalhes técnicos do HTTP/2 CONTINUATION Flood
(nowotarski.info)- HTTP/2 CONTINUATION Flood é uma classe de vulnerabilidades de implementação em que o envio contínuo de frames de cabeçalho sem
END_HEADERSpode derrubar a disponibilidade do servidor - Como a requisição do ataque não é concluída, ela não aparece nos logs de acesso HTTP, e identificar a causa pode exigir análise dos bytes do tráfego bruto
- Dependendo da implementação, o impacto varia entre esgotamento de CPU, OOM com múltiplas conexões, OOM com uma única conexão e até crash por bug no momento da desconexão
- Nos casos de Go, Firefox e Node.js, apareceram respectivamente a continuidade da decodificação HPACK, a ausência de limite para o tamanho dos cabeçalhos de resposta e o conflito entre desconexão durante o processamento de
CONTINUATIONe a atualização do contador de memória - Diferentemente do Rapid Reset, em muitas implementações era possível causar crash no servidor com uma única conexão TCP, o que significava potencial impacto sobre uma ampla variedade de serviços de internet que usam HTTP/2
Como frames CONTINUATION são usados no HTTP/2
- HTTP/2 é um protocolo que troca frames binários, em vez de linhas de texto como no HTTP/1.1
- Frames
HEADERSenviam os cabeçalhos HTTP de requisições e respostas, e os dados de cabeçalho ficam armazenados em um field block fragment codificado comHPACK - O frame
HEADERSpossui flags que indicam o fim dos cabeçalhos e do streamEND_HEADERS: sinaliza que aquele frame contém todos os cabeçalhos a serem enviadosEND_STREAM: sinaliza que não haverá mais corpo na requisição ou resposta
- Os frames têm um tamanho máximo definido no início da comunicação, e se um frame recebido ultrapassar o tamanho permitido, a conexão é encerrada com erro de protocolo
- Se não for possível colocar todos os cabeçalhos em um único frame
HEADERS, framesCONTINUATIONsão enviados em sequência após umHEADERSsemEND_HEADERS- frame
HEADERSsemEND_HEADERS - frames
CONTINUATIONadicionais semEND_HEADERS END_HEADERSdefinido no último frameCONTINUATION
- frame
- Após o último frame de cabeçalho, vem um frame
DATAcom os dados da requisição ou o stream HTTP/2 é encerrado
O núcleo da vulnerabilidade: um stream de cabeçalhos que nunca termina
- Se o cliente iniciar um novo stream HTTP/2 e então enviar frames
HEADERSeCONTINUATIONsem nunca definirEND_HEADERS, o servidor tentará continuar analisando e armazenando um stream infinito de cabeçalhos - Servidores HTTP/1.1 normalmente têm dois mecanismos para impedir cabeçalhos infinitos
- limite de tamanho de cabeçalhos que encerra a conexão quando a lista de cabeçalhos ultrapassa o tamanho permitido
- timeout de requisição/cabeçalho que encerra a conexão se a requisição ou os cabeçalhos não forem enviados a tempo
- Em várias implementações de HTTP/2, essas proteções não existiam ou estavam incorretas, incluindo Apache httpd, Envoy e vários pacotes e codecs HTTP/2
- Os resultados variavam por implementação em quatro categorias
- Esgotamento de CPU: ao continuar lendo e decodificando cabeçalhos adicionais, o uso de CPU cresce e outras respostas ficam lentas ou bloqueadas
- OOM baseado em múltiplas conexões: os cabeçalhos
CONTINUATIONficam armazenados em memória e, embora exista limite para o tamanho da lista de cabeçalhos, a ausência de timeout faz cada conexão reter memória continuamente - OOM com uma única conexão: algumas implementações continuam lendo cabeçalhos até a memória encher e o SO encerrar o processo
- Crash com poucos frames: quando a conexão cai no meio do stream
CONTINUATION, um bug de implementação faz o servidor falhar
- Sem
END_HEADERS, a requisição não é encerrada corretamente, então as requisições do cliente malicioso não são registradas nos logs de acesso
Caso do Go: esgotamento de CPU porque a decodificação HPACK não para
- Go é um caso marcante de esgotamento de CPU no
CONTINUATIONFlood - A implementação em Go agrupa um frame
HEADERS, zero ou mais framesCONTINUATIONe o decodificador HPACK na abstraçãohttp2MetaHeadersFrame readMetaFramechamaSetEmitEnabled(false)quando atinge o limite de tamanho dos cabeçalhos ou ocorre um erro, interrompendo a emissão dos cabeçalhos decodificados- Porém, mesmo depois de parar a emissão dos cabeçalhos, o decodificador HPACK continua decodificando os bytes de entrada
- O loop que fornece os frames só para quando
HeadersEnded()se tornatrue, o que acontece quando a flagEND_HEADERSé definida - Se o atacante não enviar
END_HEADERS,readMetaFramenunca retorna, e o decodificador HPACK continua processando novos bytes enquanto eles forem enviados
Casos de OOM e impacto no cliente Firefox
- O OOM ocorre em implementações que não limitam o tamanho da lista de cabeçalhos formada pelos frames
CONTINUATION - Em implementações sem timeout de cabeçalho, era possível causar crash no servidor com uma única conexão HTTP/2
- Mesmo em implementações com idle timeout, era possível fazer várias conexões HTTP/2 ocuparem quase o limite de RAM por conexão e depois manter essas conexões vivas enviando o último frame
CONTINUATIONbyte a byte a cada poucos segundos - O
CONTINUATIONFlood pode ocorrer não apenas em servidores, mas também no lado do cliente, como em navegadores - O commit de correção do Mozilla Firefox adicionou uma verificação que retorna erro de sessão
PROTOCOL_ERRORse a soma do tamanho agregado dos cabeçalhos com o tamanho do novo frame ultrapassarnetwork_http_max_response_header_size()
Caso do Node.js: crash por assertion durante desconexão
- O Node.js lidava adequadamente com o stream infinito de frames
CONTINUATION, mas ocorria uma data race quando a conexão era encerrada no meio do stream de cabeçalhos - Durante a execução do código de ataque, o Node.js caía com falha de assertion
CHECK_EQ(current_nghttp2_memory_, 0)emHttp2Session::~Http2Session() - O crash estava ligado ao instante exato em que o cliente HTTP/2 encerrava a conexão com o servidor Node.js, e a assertion ficava no destrutor de
Http2Session - O Node.js embute a biblioteca nghttp2 para tratar conexões HTTP/2
current_nghttp2_memory_rastreia a memória alocada internamente no nghttp2 e, apóssession_.reset()no destrutor, verifica se todos os artefatos do nghttp2 foram removidos da memória- A investigação mostrou que havia casos em que callbacks do nghttp2 e
reset()eram executados juntos durante o parsing de framesCONTINUATION- um frame
CONTINUATIONchegava no estadoNGHTTP2_IB_EXPECT_CONTINUATION - o estado mudava para
NGHTTP2_IB_READ_HEADER_BLOCK - o fluxo seguia por
session_after_header_block_received,session_call_on_frame_receivedeon_frame_recv_callback - o Node.js atualizava o contador de memória em
OnFrameReceiveeHandleHeadersFrame
- um frame
- Se
HandleHeadersFrameeHttp2Session::~Http2Session()executarem ao mesmo tempo,current_session_memory_é atualizado simultaneamente,current_nghttp2_memory_fica negativo eCHECK_EQfalha
Diferença em relação às vulnerabilidades de HTTP/2 de 2019
- O conjunto de vulnerabilidades de HTTP/2 relatado por Netflix e Google em 2019 está resumido na Vulnerability Note do CERT/CC VU#605641
- A CVE-2019-9516, “0-Length Headers Leak”, é um problema em que algumas implementações alocam memória para nomes e valores de cabeçalho de tamanho zero e mantêm essa memória até o fim da sessão
- O
CONTINUATIONFlood não usa cabeçalhos vazios, mas sim muitos cabeçalhos aleatórios até o limite de tamanho de frame configurado no servidor - A CVE-2019-9518, “Empty Frame Flooding”, é um problema em que frames
DATA,HEADERS,CONTINUATION,PUSH_PROMISEe outros com payload vazio são enviados sem end-of-stream, fazendo a outra ponta gastar tempo de processamento desproporcional à banda usada no ataque - O
CONTINUATIONFlood não usa frames vazios, mas frames tão grandes quanto possível para ocupar memória e consumir ciclos de CPU durante a decodificação
Por que poderia ser mais grave que o Rapid Reset
- Em outubro de 2023, foram divulgados os detalhes do zero-day de protocolo HTTP/2 chamado “Rapid Reset”, descrito como “o maior ataque DDoS já visto até hoje”
- O Rapid Reset usa a combinação de um frame
HEADERScomEND_STREAMeEND_HEADERSdefinidos e um frameRST_STREAM - Nesse modelo, mitigações padrão como rate limiting podem reduzir os danos, e administradores de servidor conseguem ver muitos requests de entrada nos logs e receber alertas
- No
CONTINUATIONFlood, como não existeEND_HEADERS, nenhuma única requisição chega a ser concluída, e os administradores não conseguem ver requests nos logs - Em muitas implementações, o
CONTINUATIONFlood podia causar crash no servidor com apenas uma única conexão TCP, e em alguns casos com pouquíssimos dados - O Rapid Reset foi usado em ataques DDoS e, na maioria dos casos, um ataque bem-sucedido exigia uma botnet
Impacto potencial sobre serviços de internet
- Segundo o Cloudflare Radar, o tráfego HTTP/2 representa cerca de 60% do tráfego HTTP humano, excluindo bots
- O Cloudflare Radar estima que o tráfego HTTP representa mais de 70% de toda a transmissão na internet
- Considerando a importância dos projetos afetados e a facilidade de exploração, uma grande parte da internet estava exposta a essa vulnerabilidade
- HTTP é usado não só em sites, mas também em muitas APIs RESTful
- Problemas de disponibilidade em APIs e sites críticos de empresas e governos podem causar perdas de milhões de dólares ou grande confusão
- Se a falha tivesse sido explorada, a depuração poderia ser muito difícil para administradores sem conhecimento de HTTP/2
- a requisição HTTP maliciosa não é encerrada corretamente
- a requisição não aparece nos logs de acesso do servidor
- a maioria dos servidores HTTP/2 não tem recursos avançados de análise de frames
- pode ser necessário analisar manualmente os dados brutos da conexão
Divulgação coordenada e resposta do CERT/CC
- Essa classe de vulnerabilidades representava um risco considerável para a segurança da internet
- Após o reporte em janeiro de 2024, o CERT/CC abriu um caso de Vulnerability Coordination para acompanhar o problema
- Várias grandes empresas de tecnologia e projetos open source participaram do processo de divulgação responsável relacionado ao tema
- Como é difícil para um único pesquisador verificar todas as implementações, problemas que afetam vários fornecedores exigem Vulnerability Coordination
- O CERT/CC publicou a Vulnerability Note sobre o problema, e esse tipo de nota é publicado apenas algumas vezes por ano
1 comentários
Comentários do Hacker News
No mês passado, mitigamos exatamente esse problema no Bandit
https://github.com/mtrudel/bandit/blob/main/lib/bandit/http2...
Do ponto de vista de quem implementa, sinceramente, é algo óbvio demais para não bloquear. Eu já vinha me preocupando com isso havia muito tempo e achava que outras implementações naturalmente também estariam se defendendo
Nos últimos meses, verifiquei dezenas de implementações e, estranhamente, até os principais servidores HTTP/2 não tinham essa proteção ou a tinham implementado de forma incorreta
Essencialmente, vejo isso como resultado de uma cultura de desenvolvimento acostumada a expandir tudo automaticamente de forma dinâmica e a não se preocupar com o quanto o tamanho pode crescer
Esse tipo de problema não se limita ao HTTP/2, mas é bem provável que a enorme complexidade do HTTP/2 tenha contribuído. Na época do HTTP/1.x, havia mais desenvolvedores acostumados a linguagens como C, então eles prestavam atenção constante ao gerenciamento do tamanho dos buffers e não teriam deixado as alocações de cabeçalhos crescerem indefinidamente numa requisição inteira, mesmo que alguns KB bastassem
Muitos ataques de negação de serviço, como slowloris e colisões de hash em parâmetros de consulta, se tornam reais porque o uso limitado de recursos só é considerado depois
> Sem impacto: Nginx, Jetty, HAProxy, NetScaler, Varnish. [0]0: https://nowotarski.info/http2-continuation-flood/
Teria sido mais robusto se ao menos a proposta de proibi-lo depois de um frame HEADERS que não estivesse cheio tivesse sido aceita, mas achou-se que isso poderia dificultar a própria tarefa de codificação. Por causa de problemas como fronteiras de bytes do compressor
É engraçado ver as mesmas coisas sendo “redescobertas” a cada 10 anos. Recentemente foi o já conhecido flood de RESET_STREAM, agora é CONTINUATION e em breve devem aparecer frames DATA de comprimento 0, WINDOW_UPDATE de 1 byte, SETTINGS INITIAL_WINDOW que consomem muita CPU e coisas do tipo. Se for possível apenas dar um nome e, de preferência, um logotipo a um problema conhecido, o mundo continua girando nesse circo de segurança
Um post anterior do mesmo autor que organiza os servidores web/proxies reversos afetados
https://nowotarski.info/http2-continuation-flood/
Este artigo ficou no topo o dia inteiro
Fiquei curioso: para um site com pouco tráfego, talvez seja mais seguro simplesmente operar com HTTP/1.1?
HTTP/2 e HTTP/3 são bem diferentes em termos de funcionalidade. Com a adição de multiplexação, janelamento, HPACK etc., as conexões quase sem estado do HTTP/1.1 viraram conexões com estado. Para manter uma conexão com estado, é preciso armazenar dados como estado e configurações, e é daí que surgem problemas como estes
No HTTP/2, a multiplexação é adicionada, então as características de defesa também mudam. Por exemplo, se a conexão vem de uma requisição de origem de um CDN, você pode permitir poucas conexões e manter um grande pool de canais multiplexados em cada uma; mas, se for acesso direto de usuários, talvez queira permitir muitas conexões, mas reduzir o número de canais multiplexados por conexão. No HTTP/1, quase tudo parecia parecido, então a defesa era muito mais simples
O HTTP/1 tem muitas condições de contorno pouco visíveis e comportamentos de exceção antigos. O formato textual é muito mais flexível do que parece quando se veem apenas cabeçalhos válidos, e também há recursos ambíguos como cabeçalhos de várias linhas, recursos MIME antigos, condições de corrida com 100-continue, cabeçalhos hop-by-hop personalizados e corpo em GET
Felizmente, os RFCs HTTP mais novos documentam muitas armadilhas. Implementar olhando apenas o RFC 2616 não resulta em uma implementação segura
O tamanho real de uma requisição ou resposta pode ser especificado simultaneamente de várias maneiras, e os valores podem entrar em conflito. Além disso, ele depende de várias combinações de recursos e de valores de cabeçalhos que exigem regras de parsing estranhas por compatibilidade retroativa, de modo que uma implementação HTTP “simples” pode ser enganada por request smuggling
De qualquer forma, é preciso uma implementação madura, robusta e bem testada
Para servir assets estáticos comuns, a única vantagem é que o HTTP/1 sofre com limite de conexões por domínio, então dá para carregar mais assets em paralelo. Usar CDNs em domínios diferentes normalmente também contorna esse problema
Em tese, seria possível servir assets JavaScript não empacotados com HTTP/2, mas nunca vi isso em produção. Provavelmente porque, na maioria dos casos, ainda é necessária uma etapa de compilação
Se fizer isso devagar, dá para chamar de slowloris v2 :(
HTTP/2, ou como enfiar à força um “upgrade” da camada de transporte em um protocolo da camada de aplicação