- HTTP/2 CONTINUATION Flood es una familia de vulnerabilidades en implementaciones de HTTP/2 que puede derribar la disponibilidad de un servidor al seguir enviando frames de encabezados sin
END_HEADERS - Como la solicitud atacante no se completa, no queda en los logs de acceso HTTP, y para identificar la causa puede ser necesario analizar los bytes del tráfico sin procesar
- Según la implementación, el alcance del impacto varía entre agotamiento de CPU, OOM con múltiples conexiones, OOM con una sola conexión e incluso crasheos por bugs en el momento de cierre de la conexión
- En los casos de Go, Firefox y Node.js se observaron, respectivamente, decodificación HPACK continua, ausencia de límite al tamaño de encabezados de respuesta y un conflicto entre el cierre de la conexión y la actualización de contadores de memoria durante el manejo de
CONTINUATION - A diferencia de Rapid Reset, en muchas implementaciones era posible crashear el servidor con una sola conexión TCP, por lo que una amplia variedad de servicios de Internet que usan HTTP/2 pudo verse afectada
Cómo se usa el frame CONTINUATION en HTTP/2
- HTTP/2 no intercambia líneas de texto como HTTP/1.1, sino que es un protocolo que intercambia frames binarios
- El frame
HEADERSenvía los encabezados HTTP de solicitudes y respuestas, y los datos de encabezados se almacenan en un field block fragment codificado conHPACK - El frame
HEADERStiene flags que indican el final de los encabezados y del streamEND_HEADERS: señal de que ese frame contiene todos los encabezados que se quieren enviarEND_STREAM: señal de que ya no hay cuerpo de solicitud o respuesta
- Los frames tienen un tamaño máximo definido al inicio de la comunicación; si un frame recibido supera el tamaño permitido, la conexión se corta por error de protocolo
- Si no es posible incluir todos los encabezados en un único frame
HEADERS, a unHEADERSsinEND_HEADERSle siguen framesCONTINUATION- Frame
HEADERSsinEND_HEADERS - Frames
CONTINUATIONadicionales sinEND_HEADERS END_HEADERSconfigurado en el último frameCONTINUATION
- Frame
- Después del último frame de encabezados, llega un frame
DATAcon los datos de la solicitud o termina el stream HTTP/2
Núcleo de la vulnerabilidad: un stream de encabezados que nunca termina
- Si un cliente inicia un nuevo stream HTTP/2 y luego envía frames
HEADERSyCONTINUATIONsin configurar nuncaEND_HEADERS, el servidor intenta seguir parseando y almacenando un stream infinito de encabezados - Los servidores HTTP/1.1 generalmente cuentan con dos mecanismos para evitar encabezados infinitos
- Un límite de tamaño de encabezados que corta la conexión si la lista de encabezados supera el tamaño permitido
- Un timeout de solicitud/encabezados que corta la conexión si la solicitud o los encabezados no se transmiten a tiempo
- Varias implementaciones de HTTP/2 no tenían estas protecciones o las habían implementado mal, incluidos Apache httpd, Envoy y varios paquetes y códecs de HTTP/2
- Los resultados según la implementación se dividen en cuatro categorías
- Agotamiento de CPU: al leer y decodificar encabezados adicionales, aumenta el uso de CPU y las respuestas a otras solicitudes se vuelven lentas o se bloquean
- OOM basado en múltiples conexiones: los encabezados
CONTINUATIONse almacenan en memoria y, aunque existe un límite de tamaño para la lista de encabezados, al no haber timeout de encabezados cada conexión sigue ocupando memoria - OOM basado en una sola conexión: algunas implementaciones siguen leyendo encabezados hasta llenar la memoria, lo que hace que el SO termine el proceso
- Crasheo con apenas unos frames: cuando la conexión se corta en medio de un stream
CONTINUATION, un bug de implementación hace crashear el servidor
- Si no hay
END_HEADERS, la solicitud no se cierra correctamente, por lo que la solicitud del cliente malicioso no se guarda en los logs de acceso
Caso Go: agotamiento de CPU porque la decodificación HPACK no se detiene
- Go es un caso destacado de agotamiento de CPU en
CONTINUATIONFlood - La implementación de Go agrupa un frame
HEADERS, cero o más framesCONTINUATIONy el decodificador HPACK mediante la abstracciónhttp2MetaHeadersFrame - Cuando
readMetaFramealcanza el límite de tamaño de encabezados o se produce un error, llama aSetEmitEnabled(false)para dejar de emitir los encabezados decodificados - Sin embargo, incluso después de detener la emisión de encabezados, el decodificador HPACK sigue decodificando los bytes de entrada
- El bucle que suministra frames solo se detiene cuando
HeadersEnded()pasa a sertrue, lo que ocurre cuando se configura el flagEND_HEADERS - Si el atacante no envía
END_HEADERS,readMetaFrameno retorna y el decodificador HPACK sigue procesando nuevos bytes mientras el atacante los envía
Casos OOM e impacto en el cliente Firefox
- El OOM ocurre en implementaciones que no limitaban el tamaño de la lista de encabezados creada por frames
CONTINUATION - En implementaciones sin timeout de encabezados, era posible crashear un servidor con una sola conexión HTTP/2
- Incluso en implementaciones con idle timeout, era posible hacer que varias conexiones HTTP/2 ocuparan una cantidad de RAM cercana al límite por conexión y luego mantenerlas vivas enviando el último frame
CONTINUATIONbyte por byte cada pocos segundos CONTINUATIONFlood puede producirse no solo en servidores, sino también del lado del cliente, como navegadores- El commit de corrección de Mozilla Firefox agregó una verificación que devuelve un error de sesión
PROTOCOL_ERRORsi la suma del tamaño agregado de encabezados y el tamaño del nuevo frame superanetwork_http_max_response_header_size()
Caso Node.js: crasheo por assertion durante el cierre de la conexión
- Node.js manejaba correctamente el stream infinito de frames
CONTINUATIONen sí, pero se producía una data race cuando la conexión se cortaba durante el stream de encabezados - Durante la ejecución del código de ataque, Node.js crasheaba por una falla de assertion
CHECK_EQ(current_nghttp2_memory_, 0)enHttp2Session::~Http2Session() - El crasheo estaba relacionado con el momento exacto en que el cliente HTTP/2 cortaba la conexión con el servidor Node.js, y la assertion estaba dentro del destructor de
Http2Session - Node.js incorpora la biblioteca nghttp2 para manejar conexiones HTTP/2
current_nghttp2_memory_rastrea la memoria asignada internamente por nghttp2 y, después desession_.reset()en el destructor, verifica que todos los artefactos de nghttp2 se hayan eliminado de la memoria- La investigación mostró que había casos en los que callbacks de nghttp2 y
reset()se ejecutaban juntos durante el parseo de framesCONTINUATION- Llega un frame
CONTINUATIONen el estadoNGHTTP2_IB_EXPECT_CONTINUATION - El estado cambia a
NGHTTP2_IB_READ_HEADER_BLOCK - Sigue el flujo
session_after_header_block_received,session_call_on_frame_received,on_frame_recv_callback OnFrameReceiveyHandleHeadersFramede Node.js actualizan el contador de memoria
- Llega un frame
- Si
HandleHeadersFrameyHttp2Session::~Http2Session()se ejecutan al mismo tiempo,current_session_memory_se actualiza simultáneamente, el valor decurrent_nghttp2_memory_se vuelve negativo yCHECK_EQfalla
Diferencias con las vulnerabilidades de HTTP/2 de 2019
- El conjunto de vulnerabilidades de HTTP/2 reportado por Netflix y Google en 2019 está resumido en la CERT/CC Vulnerability Note VU#605641
- CVE-2019-9516, “0-Length Headers Leak”, es un problema en el que algunas implementaciones asignan memoria para nombres y valores de encabezado de longitud 0 y la mantienen hasta que termina la sesión
CONTINUATIONFlood no usa encabezados vacíos, sino que envía muchos encabezados aleatorios hasta el límite de tamaño de frame configurado por el servidor- CVE-2019-9518, “Empty Frame Flooding”, consiste en enviar frames
DATA,HEADERS,CONTINUATION,PUSH_PROMISE, etc. con payload vacío y sin end-of-stream, haciendo que el par invierta un tiempo de procesamiento desproporcionado respecto del ancho de banda del ataque CONTINUATIONFlood no usa frames vacíos, sino frames lo más grandes posible para ocupar memoria y consumir ciclos de CPU durante el proceso de decodificación
Por qué pudo haber sido más grave que Rapid Reset
- En octubre de 2023 se publicaron detalles de “Rapid Reset”, un zero-day del protocolo HTTP/2, y se lo describió como “el mayor ataque DDoS hasta la fecha”
- Rapid Reset usa una combinación de frames
HEADERSconEND_STREAMyEND_HEADERSconfigurados, y framesRST_STREAM - En este método, mitigaciones estándar como rate limiting pueden reducir el daño, y los administradores del servidor pueden ver muchas solicitudes entrantes en los logs y recibir alertas
- En
CONTINUATIONFlood no hayEND_HEADERS, por lo que no se completa ni una sola solicitud, y los administradores no pueden ver las solicitudes en los logs - En muchas implementaciones,
CONTINUATIONFlood podía crashear el servidor con una sola conexión TCP, y en algunos casos bastaban muy pocos datos - Rapid Reset se usó en ataques DDoS y, en la mayoría de los casos, un ataque exitoso requería una botnet
Impacto que pudo haber tenido en servicios de Internet
- Según Cloudflare Radar, el tráfico HTTP/2 representa alrededor del 60% del tráfico HTTP humano, excluyendo bots
- Cloudflare Radar estima que el tráfico HTTP representa más del 70% de toda la transmisión de Internet
- Considerando la importancia de los proyectos afectados y la facilidad de explotación, una gran parte de Internet estuvo expuesta a esta vulnerabilidad
- HTTP se usa no solo en sitios web, sino también en muchas API RESTful
- Los problemas de disponibilidad en API y sitios web importantes de empresas y gobiernos pueden generar pérdidas o caos por millones de dólares
- Si se hubiera explotado, habría podido ser muy difícil de depurar para administradores de servidores sin conocimientos de HTTP/2
- La solicitud HTTP maliciosa no se cierra correctamente
- La solicitud no aparece en los logs de acceso del servidor
- La mayoría de los servidores HTTP/2 carecen de funciones avanzadas de análisis de frames
- Es necesario analizar manualmente los datos sin procesar de la conexión
Divulgación coordinada y respuesta de CERT/CC
- Esta familia de vulnerabilidades representaba un riesgo considerable para la seguridad de Internet
- Después del reporte de enero de 2024, CERT/CC abrió un caso de Vulnerability Coordination para hacer seguimiento del problema
- Varias grandes empresas tecnológicas y proyectos open source participaron en el proceso de divulgación responsable relacionado
- Como es difícil que un solo investigador revise todas las implementaciones, los problemas que afectan a múltiples proveedores requieren Vulnerability Coordination
- CERT/CC publicó una Vulnerability Note sobre este problema, y solo se publican unas pocas notas de este tipo cada año
1 comentarios
Opiniones de Hacker News
El mes pasado mitigamos exactamente este problema en Bandit
https://github.com/mtrudel/bandit/blob/main/lib/bandit/http2...
Desde el punto de vista de quien implementa, honestamente es algo demasiado obvio que hay que bloquear. Lo teníamos en mente desde hace mucho y asumíamos que otras implementaciones también se estarían defendiendo
En los últimos meses revisé decenas de implementaciones y, curiosamente, incluso los principales servidores HTTP/2 no tenían esta protección o la tenían mal implementada
En esencia, lo veo como resultado de una cultura de desarrollo acostumbrada a escalar todo de forma dinámica y automática, sin preocuparse por qué tan grande puede llegar a ser
Este tipo de problema no se limita a HTTP/2, pero es muy probable que la enorme complejidad de HTTP/2 haya contribuido. En la época de HTTP/1.x había más desarrolladores acostumbrados a lenguajes como C, que prestaban atención constante al manejo de longitudes de búfer, y no habrían dejado que algo creciera indefinidamente cuando, para toda una solicitud, unas cuantas KB de asignación de encabezados bastaban de sobra
Muchos ataques de denegación de servicio, como slowloris o las colisiones de hash en parámetros de consulta, se volvieron reales porque el uso acotado de recursos se consideró demasiado tarde
> No afectados: Nginx, Jetty, HAProxy, NetScaler, Varnish. [0]0: https://nowotarski.info/http2-continuation-flood/
Habría sido más robusto si al menos se hubiera aceptado la propuesta de prohibirlas después de un frame HEADERS que no estuviera lleno, pero se consideró que eso podía volver más difícil el trabajo de codificación. Por temas como los límites de bytes del compresor
Da risa ver cómo cada 10 años se “redescubren” las mismas cosas. Hace poco fue el conocido flood de RESET_STREAM, ahora es CONTINUATION, y pronto quizá vengan los frames DATA de longitud 0, WINDOW_UPDATE de 1 byte o SETTINGS de INITIAL_WINDOW que hagan consumir mucho CPU. Si a un problema conocido se le puede poner un nombre y, si es posible, un logo, el mundo seguirá girando este circo de seguridad
Un artículo anterior del mismo autor que resume los servidores web/proxies inversos afectados
https://nowotarski.info/http2-continuation-flood/
Este artículo estuvo todo el día en la parte más alta
Me da curiosidad: si un sitio web tiene poco tráfico, ¿tal vez sería más seguro simplemente operarlo con HTTP/1.1?
HTTP/2 y HTTP/3 son muy distintos en funcionalidades. Con la multiplexación, el windowing, HPACK, etc., la conexión casi sin estado de HTTP/1.1 se convierte en una conexión con estado. Para mantener una conexión con estado hay que guardar datos como el estado y la configuración, y de ahí surgen estos problemas
En HTTP/2 se agrega multiplexación, así que también cambian las características defensivas. Por ejemplo, si la conexión viene de solicitudes de origen de un CDN, podrías permitir pocas conexiones y un gran pool de canales multiplexados por conexión; pero si es acceso directo de usuarios, quizá quieras permitir muchas conexiones y reducir la cantidad de canales multiplexados por conexión. En HTTP/1 casi todo se veía parecido, por lo que la defensa era mucho más simple
HTTP/1 tiene muchas condiciones de borde poco visibles y comportamientos heredados de excepción. El formato de texto es mucho más flexible de lo que parece cuando solo ves encabezados válidos, y también hay funciones ambiguas como encabezados multilínea, funciones MIME antiguas, condiciones de carrera con 100-continue, encabezados hop-by-hop personalizados y cuerpos en GET
Por suerte, los nuevos RFC de HTTP documentan muchas trampas. Si implementas basándote solo en RFC 2616, no obtendrás una implementación segura
El tamaño real de una solicitud o respuesta puede especificarse simultáneamente de varias maneras, con valores que incluso pueden entrar en conflicto. Además, depende de distintas combinaciones de funcionalidades y de valores de encabezado que requieren reglas de parsing extrañas por compatibilidad hacia atrás, de modo que una implementación “simple” de HTTP puede ser engañada con request smuggling
En cualquier caso se necesita una implementación madura, robusta y bien probada
Para servir recursos estáticos comunes, la única ventaja de HTTP/1 es que, como tiene límites de conexiones por dominio, permite cargar más recursos en paralelo. Si usas CDNs en dominios distintos, normalmente también puedes evitar ese problema
En teoría podrías servir recursos JavaScript sin empaquetar con HTTP/2, pero no lo he visto en entornos reales de producción. Probablemente porque en la mayoría de los casos sigue siendo necesario un paso de compilación
Si haces esto lentamente, podrías llamarlo slowloris v2 :(
HTTP/2, o cómo meter a la fuerza una “actualización” de la capa de transporte en un protocolo de capa de aplicación