- HTTP/2 CONTINUATION Flood est une famille de vulnérabilités d’implémentation HTTP/2 capable de faire chuter la disponibilité d’un serveur en envoyant en continu des frames d’en-tête sans
END_HEADERS - Comme la requête d’attaque n’est jamais terminée, elle n’apparaît pas dans les logs d’accès HTTP, et l’identification de la cause peut nécessiter une analyse des octets du trafic brut
- Selon l’implémentation, l’impact varie entre épuisement CPU, OOM via plusieurs connexions, OOM sur une seule connexion, voire crash dû à un bug de timing lors de la déconnexion
- Les cas de Go, Firefox et Node.js ont respectivement révélé une poursuite du décodage HPACK, l’absence de limite sur la taille des en-têtes de réponse, et un conflit entre la déconnexion pendant le traitement de
CONTINUATIONet la mise à jour du compteur mémoire - Contrairement à Rapid Reset, de nombreuses implémentations pouvaient être plantées via une seule connexion TCP, ce qui signifiait qu’un large ensemble de services Internet utilisant HTTP/2 pouvait être affecté
Utilisation des frames CONTINUATION dans HTTP/2
- HTTP/2 est un protocole qui échange des frames binaires au lieu de lignes de texte comme HTTP/1.1
- Les frames
HEADERStransportent les en-têtes HTTP des requêtes et des réponses, et les données d’en-tête sont stockées dans un field block fragment encodé avecHPACK - Une frame
HEADERScontient des drapeaux indiquant la fin des en-têtes et du fluxEND_HEADERS: signale que la frame contient tous les en-têtes à envoyerEND_STREAM: signale qu’il n’y a plus de corps de requête ou de réponse
- Les frames ont une taille maximale définie au début de la communication, et si une frame reçue dépasse cette taille autorisée, la connexion est fermée pour erreur de protocole
- Si tous les en-têtes ne tiennent pas dans une seule frame
HEADERS, celle-ci est suivie de framesCONTINUATIONsansEND_HEADERS- une frame
HEADERSsansEND_HEADERS - des frames
CONTINUATIONsupplémentaires sansEND_HEADERS - la dernière frame
CONTINUATIONavecEND_HEADERS
- une frame
- Après la dernière frame d’en-tête, une frame
DATAcontenant les données de la requête peut suivre, ou le flux HTTP/2 peut se terminer
Le cœur de la vulnérabilité : un flux d’en-têtes sans fin
- Si un client ouvre un nouveau flux HTTP/2 puis envoie des frames
HEADERSetCONTINUATIONsans jamais définirEND_HEADERS, le serveur continue d’essayer de parser et de stocker un flux infini d’en-têtes - Les serveurs HTTP/1.1 disposent généralement de deux mécanismes pour empêcher des en-têtes infinis
- une limite de taille des en-têtes qui coupe la connexion si la liste d’en-têtes dépasse la taille autorisée
- un timeout de requête / d’en-têtes qui coupe la connexion si la requête ou les en-têtes ne sont pas transmis à temps
- Plusieurs implémentations HTTP/2 n’avaient pas ces protections, ou les implémentaient mal, notamment Apache httpd, Envoy, ainsi que plusieurs packages et codecs HTTP/2
- Selon l’implémentation, les conséquences se répartissent en quatre catégories
- Épuisement CPU : la lecture et le décodage d’en-têtes supplémentaires augmentent l’utilisation CPU, ce qui ralentit ou bloque les réponses aux autres requêtes
- OOM multi-connexions : les en-têtes
CONTINUATIONsont stockés en mémoire ; il existe une limite de taille de liste d’en-têtes, mais pas de timeout d’en-têtes, ce qui fait que chaque connexion continue d’occuper de la mémoire - OOM sur une seule connexion : certaines implémentations continuent de lire les en-têtes jusqu’à remplir la mémoire, laissant l’OS tuer le processus
- crash en quelques frames : un bug d’implémentation fait planter le serveur si la connexion se coupe au milieu d’un flux
CONTINUATION
- Sans
END_HEADERS, la requête n’est jamais correctement close, donc les requêtes du client malveillant ne sont pas enregistrées dans les logs d’accès
Cas Go : épuisement CPU car le décodage HPACK ne s’arrête pas
- Go est un exemple marquant d’épuisement CPU face à
CONTINUATIONFlood - L’implémentation Go regroupe une frame
HEADERS, zéro ou plusieurs framesCONTINUATION, et un décodeur HPACK dans l’abstractionhttp2MetaHeadersFrame readMetaFrameappelleSetEmitEnabled(false)lorsqu’une limite de taille d’en-tête est atteinte ou qu’une erreur survient, afin d’arrêter l’émission des en-têtes décodés- Mais même après l’arrêt de cette émission, le décodeur HPACK continue de décoder les octets entrants
- La boucle qui alimente les frames ne s’arrête que lorsque
HeadersEnded()devienttrue, ce qui n’arrive que lorsque le drapeauEND_HEADERSest défini - Si l’attaquant n’envoie jamais
END_HEADERS,readMetaFramene retourne jamais, et le décodeur HPACK continue de traiter de nouveaux octets tant que l’attaquant en envoie
Cas OOM et impact côté client dans Firefox
- Les OOM apparaissent dans les implémentations qui ne limitaient pas la taille de la liste d’en-têtes créée par les frames
CONTINUATION - Dans les implémentations sans timeout d’en-têtes, une seule connexion HTTP/2 suffisait pour faire planter un serveur
- Même avec un idle timeout, il était possible d’ouvrir plusieurs connexions HTTP/2, de faire consommer à chacune presque sa limite de RAM, puis de maintenir les connexions en vie en envoyant la dernière frame
CONTINUATIONoctet par octet toutes les quelques secondes CONTINUATIONFlood peut affecter non seulement les serveurs, mais aussi le côté client comme les navigateurs- Le commit de correction de Mozilla Firefox a ajouté une vérification : si la somme de la taille agrégée des en-têtes et de la taille de la nouvelle frame dépasse
network_http_max_response_header_size(), une erreur de sessionPROTOCOL_ERRORest renvoyée
Cas Node.js : crash par assertion lors de la déconnexion
- Node.js gérait correctement le flux infini de frames
CONTINUATIONlui-même, mais un data race survenait si la connexion se fermait au milieu du flux d’en-têtes - Pendant l’exécution du code d’attaque, Node.js plantait dans
Http2Session::~Http2Session()avec un échec de l’assertionCHECK_EQ(current_nghttp2_memory_, 0) - Le crash était lié au moment précis où le client HTTP/2 coupait la connexion au serveur Node.js, et l’assertion se trouvait dans le destructeur de
Http2Session - Node.js embarque la bibliothèque nghttp2 pour gérer les connexions HTTP/2
current_nghttp2_memory_suit la mémoire allouée en interne par nghttp2, et aprèssession_.reset()dans le destructeur, il vérifie que tous les artefacts nghttp2 ont bien été retirés de la mémoire- L’enquête a montré que le callback nghttp2 et
reset()pouvaient s’exécuter en même temps pendant le parsing des framesCONTINUATION- une frame
CONTINUATIONarrive dans l’étatNGHTTP2_IB_EXPECT_CONTINUATION - l’état passe à
NGHTTP2_IB_READ_HEADER_BLOCK - le flot
session_after_header_block_received,session_call_on_frame_received,on_frame_recv_callbacks’enchaîne - Node.js met à jour le compteur mémoire dans
OnFrameReceiveetHandleHeadersFrame
- une frame
- Si
HandleHeadersFrameetHttp2Session::~Http2Session()s’exécutent en même temps,current_session_memory_est mis à jour concurremment,current_nghttp2_memory_devient négatif etCHECK_EQéchoue
Différences avec les vulnérabilités HTTP/2 de 2019
- L’ensemble de vulnérabilités HTTP/2 signalé en 2019 par Netflix et Google est résumé dans la Vulnerability Note CERT/CC VU#605641
- La CVE-2019-9516, « 0-Length Headers Leak », correspond à un problème où certaines implémentations allouent de la mémoire pour des noms et valeurs d’en-tête de longueur nulle et la conservent jusqu’à la fin de la session
CONTINUATIONFlood n’utilise pas des en-têtes vides, mais un grand nombre d’en-têtes aléatoires jusqu’à la limite de taille de frame configurée par le serveur- La CVE-2019-9518, « Empty Frame Flooding », consiste à envoyer des frames
DATA,HEADERS,CONTINUATION,PUSH_PROMISEau payload vide, etc., sans end-of-stream, afin que la cible consacre un temps de traitement disproportionné par rapport à la bande passante d’attaque CONTINUATIONFlood n’utilise pas des frames vides, mais des frames aussi grosses que possible pour occuper la mémoire et consommer des cycles CPU pendant le décodage
Pourquoi cela pouvait être plus grave que Rapid Reset
- En octobre 2023, les détails de « Rapid Reset », un zero-day du protocole HTTP/2, ont été rendus publics et il a été décrit comme « la plus grande attaque DDoS observée à ce jour »
- Rapid Reset utilise une combinaison d’une frame
HEADERSavecEND_STREAMetEND_HEADERS, et d’une frameRST_STREAM - Avec cette technique, des mesures d’atténuation standard comme le rate limiting peuvent réduire l’impact, et les administrateurs voient de nombreuses requêtes entrantes dans les logs, ce qui déclenche des alertes
- Avec
CONTINUATIONFlood, il n’y a pas deEND_HEADERS, donc pas une seule requête n’est jamais finalisée, et les administrateurs ne voient aucune requête dans les logs - Dans de nombreuses implémentations,
CONTINUATIONFlood pouvait faire planter le serveur avec une seule connexion TCP, et dans certains cas avec très peu de données - Rapid Reset a été utilisé pour des attaques DDoS, et dans la plupart des cas une attaque efficace nécessitait un botnet
Impact potentiel sur les services Internet
- Selon Cloudflare Radar, le trafic HTTP/2 représente environ 60 % du trafic HTTP humain, hors bots
- Cloudflare Radar estime que le trafic HTTP représente plus de 70 % de l’ensemble des transmissions sur Internet
- Compte tenu de l’importance des projets affectés et de la facilité d’exploitation, une grande partie d’Internet était exposée à cette vulnérabilité
- HTTP est utilisé non seulement pour les sites web, mais aussi par de nombreuses API RESTful
- Des problèmes de disponibilité sur des API et sites web d’entreprises ou d’administrations critiques peuvent provoquer des pertes de plusieurs millions de dollars ou un chaos important
- En cas d’exploitation, le débogage aurait pu être extrêmement difficile pour des administrateurs sans expertise HTTP/2
- les requêtes HTTP malveillantes ne sont jamais correctement closes
- elles n’apparaissent pas dans les logs d’accès du serveur
- la plupart des serveurs HTTP/2 manquent de fonctions avancées d’analyse des frames
- il faut analyser manuellement les données brutes des connexions
Divulgation coordonnée et réponse du CERT/CC
- Cette famille de vulnérabilités représentait un risque important pour la sécurité d’Internet
- Après le signalement de janvier 2024, le CERT/CC a ouvert un dossier de coordination de vulnérabilité pour suivre le problème
- Plusieurs grandes entreprises technologiques et projets open source ont participé au processus de divulgation responsable associé
- Comme il est difficile pour un chercheur seul d’examiner toutes les implémentations, une coordination des vulnérabilités était nécessaire pour ce type de problème touchant plusieurs éditeurs
- Le CERT/CC a publié une Vulnerability Note sur le sujet, un type de note qui n’est publié que quelques fois par an
1 commentaires
Commentaires sur Hacker News
Le mois dernier, Bandit a justement atténué ce problème
https://github.com/mtrudel/bandit/blob/main/lib/bandit/http2...
Du point de vue de l’implémenteur, c’est franchement le genre de chose qu’il faut évidemment bloquer. On y faisait attention depuis longtemps, et on pensait naturellement que les autres implémentations se protégeaient aussi
Ces derniers mois, j’ai vérifié des dizaines d’implémentations et, de façon assez étrange, même les principaux serveurs HTTP/2 n’avaient pas cette protection ou l’avaient mal implémentée
Au fond, j’y vois le résultat d’une culture de développement habituée à faire grossir dynamiquement et automatiquement tout ce qu’elle manipule, sans vraiment se soucier de la taille que cela peut atteindre
Ce type de problème ne se limite pas à HTTP/2, mais la forte complexité de HTTP/2 y a probablement contribué. À l’époque de HTTP/1.x, il y avait davantage de développeurs habitués à des langages comme le C, donc plus attentifs à la gestion de la taille des buffers, et ils n’auraient sans doute pas laissé l’allocation des en-têtes grandir sans limite alors que quelques KB suffisent au maximum pour l’ensemble d’une requête
Beaucoup d’attaques par déni de service, comme slowloris ou les collisions de hachage dans les paramètres de requête, ne deviennent réellement problématiques qu’une fois qu’on prend en compte, trop tard, l’usage de ressources limitées
> Non affectés : Nginx, Jetty, HAProxy, NetScaler, Varnish. [0]0: https://nowotarski.info/http2-continuation-flood/
Cela aurait été plus robuste si l’on avait au moins adopté l’idée de l’interdire après une trame HEADERS non pleine, mais on estimait que cela pouvait compliquer le travail d’encodage lui-même, notamment à cause de problèmes comme les frontières d’octets du compresseur
C’est assez drôle de voir les mêmes choses être “redécouvertes” tous les 10 ans. Récemment, c’était le désormais bien connu flood de RESET_STREAM, cette fois c’est CONTINUATION, et bientôt ce sera probablement les trames DATA de longueur 0, les WINDOW_UPDATE d’un octet, ou les paramètres INITIAL_WINDOW qui font beaucoup travailler le CPU. Tant qu’on peut coller un nom et, si possible, un logo à un problème connu, le grand cirque de la sécurité continuera de tourner
Un article précédent du même auteur recensant les serveurs web / reverse proxies affectés
https://nowotarski.info/http2-continuation-flood/
Cet article est resté tout en haut toute la journée
Je me demande si, pour un site à faible trafic, il ne serait pas plus sûr de fonctionner simplement en HTTP/1.1
HTTP/2 et HTTP/3 diffèrent fortement sur le plan fonctionnel. Avec l’ajout du multiplexage, du fenêtrage, de HPACK, etc., la connexion quasi sans état de HTTP/1.1 est devenue une connexion avec état. Maintenir une connexion avec état impose de stocker des données comme l’état et la configuration, et c’est ainsi que ce type de problèmes apparaît
En HTTP/2, l’ajout du multiplexage change aussi les propriétés défensives. Par exemple, si une connexion provient des requêtes d’origine d’un CDN, on peut vouloir autoriser peu de connexions mais avec un grand pool de canaux multiplexés par connexion ; en accès direct utilisateur, on peut au contraire vouloir autoriser beaucoup de connexions mais réduire le nombre de canaux multiplexés par connexion. En HTTP/1, tout se ressemblait davantage, donc la défense était bien plus simple
HTTP/1 regorge de cas limites peu visibles et de comportements anciens maintenus par exception. Le format texte est bien plus souple qu’on ne le croit quand on ne regarde que des en-têtes valides ; il y a aussi les en-têtes sur plusieurs lignes, d’anciennes fonctionnalités MIME, les conditions de concurrence autour de 100-continue, les en-têtes hop-by-hop personnalisés, ou encore des fonctions ambiguës comme un corps de requête GET
Heureusement, les nouveaux RFC HTTP documentent beaucoup de ces pièges. Si on implémente à partir du seul RFC 2616, on n’obtiendra pas une implémentation sûre
La taille réelle d’une requête ou d’une réponse peut être indiquée de plusieurs façons simultanément, avec des valeurs potentiellement contradictoires. Elle peut aussi dépendre de combinaisons de fonctionnalités et de valeurs d’en-tête nécessitant des règles d’analyse étranges pour rester rétrocompatibles, si bien qu’une implémentation HTTP “simple” peut se faire piéger par du request smuggling
Dans tous les cas, il faut une implémentation mûre, robuste et bien testée
Pour servir des ressources statiques classiques, HTTP/1 n’a vraiment comme avantage que d’être limité sur le nombre de connexions par domaine, ce qui permet de charger plus de ressources en parallèle. En utilisant des CDN sur différents domaines, on peut généralement contourner ce problème
En théorie, HTTP/2 permettrait de servir des ressources JavaScript non bundleées, mais je ne l’ai jamais vu en production. C’est probablement parce que, dans la plupart des cas, une étape de compilation reste nécessaire
Si on fait ça lentement, on pourrait appeler ça slowloris v2 :(
HTTP/2, ou l’art de forcer une “mise à niveau” de la couche transport dans un protocole de couche application