3 points par GN⁺ 2024-04-06 | 1 commentaires | Partager sur WhatsApp
  • 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 CONTINUATION et 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 HEADERS transportent 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é avec HPACK
  • Une frame HEADERS contient des drapeaux indiquant la fin des en-têtes et du flux
    • END_HEADERS : signale que la frame contient tous les en-têtes à envoyer
    • END_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 frames CONTINUATION sans END_HEADERS
    • une frame HEADERS sans END_HEADERS
    • des frames CONTINUATION supplémentaires sans END_HEADERS
    • la dernière frame CONTINUATION avec END_HEADERS
  • Après la dernière frame d’en-tête, une frame DATA contenant 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 HEADERS et CONTINUATION sans jamais définir END_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 CONTINUATION sont 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 à CONTINUATION Flood
  • L’implémentation Go regroupe une frame HEADERS, zéro ou plusieurs frames CONTINUATION, et un décodeur HPACK dans l’abstraction http2MetaHeadersFrame
  • readMetaFrame appelle SetEmitEnabled(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() devient true, ce qui n’arrive que lorsque le drapeau END_HEADERS est défini
  • Si l’attaquant n’envoie jamais END_HEADERS, readMetaFrame ne 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 CONTINUATION octet par octet toutes les quelques secondes
  • CONTINUATION Flood 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 session PROTOCOL_ERROR est renvoyée

Cas Node.js : crash par assertion lors de la déconnexion

  • Node.js gérait correctement le flux infini de frames CONTINUATION lui-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’assertion CHECK_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ès session_.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 frames CONTINUATION
    • une frame CONTINUATION arrive dans l’état NGHTTP2_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_callback s’enchaîne
    • Node.js met à jour le compteur mémoire dans OnFrameReceive et HandleHeadersFrame
  • Si HandleHeadersFrame et Http2Session::~Http2Session() s’exécutent en même temps, current_session_memory_ est mis à jour concurremment, current_nghttp2_memory_ devient négatif et CHECK_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
  • CONTINUATION Flood 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_PROMISE au payload vide, etc., sans end-of-stream, afin que la cible consacre un temps de traitement disproportionné par rapport à la bande passante d’attaque
  • CONTINUATION Flood 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 HEADERS avec END_STREAM et END_HEADERS, et d’une frame RST_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 CONTINUATION Flood, il n’y a pas de END_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, CONTINUATION Flood 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

 
GN⁺ 2024-04-06
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

    • On sait ce qui arrive quand on fait des suppositions. Ça finit par faire de toi et moi les gros titres de la page d’accueil
  • 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

    • Les gens continuent de se concentrer et d’optimiser uniquement le chemin nominal, sans s’arrêter pour réfléchir à ce qui se passe si un attaquant provoque délibérément et de façon répétée le pire scénario possible
      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/

    • Autrement dit, ce sont des implémentations qui s’opposent depuis 10 ans à l’usage de CONTINUATION à cause du risque de déni de service. Si on relit les longues discussions de l’époque, la question centrale était toujours comment éviter ce fichu CONTINUATION : https://lists.w3.org/Archives/Public/ietf-http-wg/2014JulSep...
      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
    • Et Caddy, alors ? C’est un excellent projet, il mérite bien sa propre ligne ;)
  • 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

    • Il est raisonnable de penser que HTTP/1.1, bien plus simple à implémenter, aura moins de bugs
      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
    • Faire une mise à niveau uniquement pour faire une mise à niveau n’est pas une bonne pratique d’ingénierie. S’il n’y a pas de bénéfice supplémentaire, elle est difficile à justifier
    • Pas forcément. Ceux qui disent que HTTP/1.1 est simple n’ont en général jamais implémenté un parseur complet compatible avec les environnements réels
      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
    • Je me posais la même question. Comme c’est plus mature et moins complexe, ça semble pouvoir être plus sûr
    • C’est probablement le cas. HTTP/2 est bien pour le streaming, et même là il est en train d’être remplacé par des protocoles plus récents
      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