- HTTP/2 CONTINUATION Flood ist eine Familie von Schwachstellen in HTTP/2-Implementierungen, bei der fortlaufend Header-Frames ohne
END_HEADERSgesendet werden, wodurch die Verfügbarkeit von Servern zusammenbrechen kann - Da die Angriffsanfrage nie abgeschlossen wird, erscheint sie nicht in HTTP-Zugriffslogs; zur Ursachenanalyse kann eine Analyse der rohen Traffic-Bytes erforderlich sein
- Je nach Implementierung reicht die Auswirkung von CPU-Erschöpfung über OOM bei mehreren Verbindungen und OOM bei einer einzelnen Verbindung bis hin zu Abstürzen durch Timing-Bugs beim Verbindungsabbruch
- In den Fällen Go, Firefox und Node.js zeigten sich jeweils fortgesetztes HPACK-Decoding, fehlende Begrenzung der Antwort-Header-Größe sowie ein Konflikt zwischen Verbindungsabbruch während der Verarbeitung von
CONTINUATIONund Aktualisierung von Speicherzählern - Anders als bei Rapid Reset konnten viele Implementierungen bereits mit einer einzigen TCP-Verbindung zum Absturz gebracht werden, sodass breite Teile der Internetdienste, die HTTP/2 nutzen, betroffen sein konnten
Wie CONTINUATION-Frames in HTTP/2 verwendet werden
- HTTP/2 tauscht, anders als HTTP/1.1, keine Textzeilen aus, sondern binäre Frames
- Ein
HEADERS-Frame überträgt die HTTP-Header von Requests und Responses; die Header-Daten werden in einem mitHPACKcodierten Field Block Fragment gespeichert - Ein
HEADERS-Frame besitzt Flags, die das Ende der Header und des Streams signalisierenEND_HEADERS: signalisiert, dass dieser Frame alle zu sendenden Header enthältEND_STREAM: signalisiert, dass es keinen weiteren Request- oder Response-Body gibt
- Frames haben eine Maximalgröße, die beim Start der Kommunikation festgelegt wird; überschreitet ein empfangener Frame die erlaubte Größe, wird die Verbindung wegen eines Protokollfehlers getrennt
- Wenn nicht alle Header in einen einzelnen
HEADERS-Frame passen, folgen auf einenHEADERSohneEND_HEADERSweitereCONTINUATION-Frames- ein
HEADERS-Frame ohneEND_HEADERS - zusätzliche
CONTINUATION-Frames ohneEND_HEADERS END_HEADERSist im letztenCONTINUATION-Frame gesetzt
- ein
- Nach dem letzten Header-Frame folgt entweder ein
DATA-Frame mit Request-Daten oder der HTTP/2-Stream endet
Kern der Schwachstelle: ein nie endender Header-Stream
- Wenn ein Client einen neuen HTTP/2-Stream startet und anschließend
HEADERS- undCONTINUATION-Frames sendet, ohne jemalsEND_HEADERSzu setzen, versucht der Server, einen unendlichen Header-Stream weiter zu parsen und zu speichern - HTTP/1.1-Server verfügen üblicherweise über zwei Mechanismen gegen unendliche Header
- ein Header-Größenlimit, das die Verbindung trennt, wenn die Header-Liste die erlaubte Größe überschreitet
- ein Request-/Header-Timeout, das die Verbindung trennt, wenn Request oder Header nicht rechtzeitig übertragen werden
- In mehreren HTTP/2-Implementierungen fehlte dieser Schutz oder war fehlerhaft umgesetzt, darunter auch Apache httpd, Envoy sowie verschiedene HTTP/2-Pakete und Codecs
- Die implementierungsabhängigen Folgen lassen sich in vier Kategorien einteilen
- CPU-Erschöpfung: Während weitere Header gelesen und decodiert werden, steigt die CPU-Auslastung, wodurch Antworten auf andere Requests langsamer werden oder blockieren
- OOM auf Basis mehrerer Verbindungen:
CONTINUATION-Header werden im Speicher gehalten; es gibt zwar ein Limit für die Header-Listengröße, aber kein Header-Timeout, sodass jede Verbindung weiter Speicher belegt - OOM auf Basis einer einzelnen Verbindung: Einige Implementierungen lesen Header weiter, bis der Speicher voll ist und das Betriebssystem den Prozess beendet
- Absturz mit nur wenigen Frames: Wenn die Verbindung mitten in einem
CONTINUATION-Stream getrennt wird, stürzt der Server aufgrund eines Implementierungsbugs ab
- Ohne
END_HEADERSwird der Request nicht korrekt abgeschlossen, daher wird der Request des bösartigen Clients nicht in Zugriffslogs gespeichert
Go-Beispiel: CPU-Erschöpfung durch nicht endendes HPACK-Decoding
- Go ist ein markantes Beispiel für CPU-Erschöpfung durch
CONTINUATIONFlood - Die Go-Implementierung bündelt über die Abstraktion
http2MetaHeadersFrameeinenHEADERS-Frame, null oder mehrCONTINUATION-Frames und den HPACK-Decoder readMetaFrameruftSetEmitEnabled(false)auf, wenn das Header-Größenlimit erreicht wird oder ein Fehler auftritt, und stoppt damit die Ausgabe decodierter Header- Doch auch nachdem die Header-Ausgabe gestoppt wurde, decodiert der HPACK-Decoder die Eingabebytes weiter
- Die Frame-Zulieferungsschleife stoppt erst, wenn
HeadersEnded()truewird; das geschieht, wenn das FlagEND_HEADERSgesetzt ist - Sendet der Angreifer kein
END_HEADERS, kehrtreadMetaFramenicht zurück, und der HPACK-Decoder verarbeitet weiter neue Bytes, solange der Angreifer sendet
OOM-Beispiele und Auswirkungen auf den Firefox-Client
- OOM tritt in Implementierungen auf, die die Größe der durch
CONTINUATION-Frames erzeugten Header-Liste nicht begrenzen - In Implementierungen ohne Header-Timeout konnte ein Server mit nur einer HTTP/2-Verbindung zum Absturz gebracht werden
- Auch bei Implementierungen mit Idle Timeout war es möglich, mehrere HTTP/2-Verbindungen jeweils nahe an ihr RAM-Limit zu bringen und die Verbindungen dann aufrechtzuerhalten, indem alle paar Sekunden das letzte
CONTINUATION-Frame byteweise gesendet wurde CONTINUATIONFlood kann nicht nur auf Servern auftreten, sondern auch auf der Client-Seite, etwa in Browsern- In einem Mozilla-Firefox-Fix-Commit wurde eine Prüfung ergänzt, die einen Sitzungsfehler mit
PROTOCOL_ERRORzurückgibt, wenn die Summe aus aggregierter Header-Größe und Größe des neuen Framesnetwork_http_max_response_header_size()überschreitet
Node.js-Beispiel: Assertion-Absturz beim Verbindungsabbruch
- Node.js verarbeitete den unendlichen Strom von
CONTINUATION-Frames selbst korrekt, doch beim Trennen der Verbindung während des Header-Streams trat eine data race auf - Während der Angriffscode lief, stürzte Node.js in
Http2Session::~Http2Session()wegen einer fehlgeschlagenen AssertionCHECK_EQ(current_nghttp2_memory_, 0)ab - Der Absturz hing mit dem exakten Zeitpunkt zusammen, zu dem der HTTP/2-Client die Verbindung zum Node.js-Server trennte; die Assertion befand sich im Destruktor von
Http2Session - Node.js bindet für die Verarbeitung von HTTP/2-Verbindungen die Bibliothek nghttp2 ein
current_nghttp2_memory_verfolgt Speicher, der intern von nghttp2 alloziert wurde, und prüft nachsession_.reset()im Destruktor, ob alle nghttp2-Artefakte aus dem Speicher entfernt wurden- Die Untersuchung ergab, dass beim Parsen von
CONTINUATION-Frames nghttp2-Callbacks undreset()gleichzeitig ausgeführt werden konnten- Im Zustand
NGHTTP2_IB_EXPECT_CONTINUATIONtrifft einCONTINUATION-Frame ein - Der Zustand wechselt zu
NGHTTP2_IB_READ_HEADER_BLOCK - Darauf folgen die Abläufe
session_after_header_block_received,session_call_on_frame_received,on_frame_recv_callback - In Node.js aktualisieren
OnFrameReceiveundHandleHeadersFrameden Speicherzähler
- Im Zustand
- Wenn
HandleHeadersFrameundHttp2Session::~Http2Session()gleichzeitig ausgeführt werden, wirdcurrent_session_memory_parallel aktualisiert, der Wert voncurrent_nghttp2_memory_wird negativ, undCHECK_EQschlägt fehl
Unterschiede zu den HTTP/2-Schwachstellen von 2019
- Das Bündel von HTTP/2-Schwachstellen, das Netflix und Google 2019 meldeten, ist in der CERT/CC Vulnerability Note VU#605641 zusammengefasst
- CVE-2019-9516, „0-Length Headers Leak“, ist ein Problem, bei dem manche Implementierungen für Header-Namen und -Werte der Länge 0 Speicher allozieren und bis zum Ende der Sitzung behalten
CONTINUATIONFlood sendet keine leeren Header, sondern viele zufällige Header bis zum vom Server gesetzten Frame-Größenlimit- CVE-2019-9518, „Empty Frame Flooding“, ist ein Problem, bei dem
DATA-,HEADERS-,CONTINUATION-,PUSH_PROMISE-Frames und andere mit leerer Payload ohne End-of-Stream gesendet werden, sodass die Gegenseite im Verhältnis zur Angriffsbandbreite übermäßig viel Verarbeitungszeit aufwendet CONTINUATIONFlood verwendet keine leeren Frames, sondern möglichst große Frames, um Speicher zu belegen und beim Decoding CPU-Zyklen zu verbrauchen
Warum es gravierender als Rapid Reset hätte sein können
- Im Oktober 2023 wurden Details zu „Rapid Reset“, einem Zero-Day im HTTP/2-Protokoll, veröffentlicht; es wurde als „größter DDoS-Angriff bisher“ bezeichnet
- Rapid Reset verwendet eine Kombination aus
HEADERS-Frames, bei denenEND_STREAMundEND_HEADERSgesetzt sind, undRST_STREAM-Frames - Bei dieser Methode können Standard-Gegenmaßnahmen wie Rate Limiting den Schaden verringern, und Serveradministratoren können viele eingehende Requests in den Logs sehen und gewarnt werden
- Bei
CONTINUATIONFlood wird wegen des fehlendenEND_HEADERSkein einziger Request abgeschlossen, und Administratoren können die Requests in den Logs nicht sehen - In vielen Implementierungen konnte
CONTINUATIONFlood einen Server bereits mit einer einzigen TCP-Verbindung zum Absturz bringen; in einigen Fällen reichten sogar sehr geringe Datenmengen - Rapid Reset wurde für DDoS-Angriffe verwendet, und in den meisten Fällen war für einen erfolgreichen Angriff ein Botnet erforderlich
Mögliche Auswirkungen auf Internetdienste
- Laut Cloudflare Radar macht HTTP/2-Traffic etwa 60 % des menschlichen HTTP-Traffics ohne Bots aus
- Cloudflare Radar schätzt, dass HTTP-Traffic mehr als 70 % der gesamten Internetübertragung ausmacht
- Angesichts der Bedeutung der betroffenen Projekte und der einfachen Ausnutzbarkeit war ein großer Teil des Internets dieser Schwachstelle ausgesetzt
- HTTP wird nicht nur für Websites, sondern auch für viele RESTful APIs verwendet
- Verfügbarkeitsprobleme bei wichtigen Unternehmens- und Regierungs-APIs sowie Websites können Verluste in Millionenhöhe oder erhebliche Störungen verursachen
- Wäre die Schwachstelle ausgenutzt worden, wäre das Debugging für Serveradministratoren ohne HTTP/2-Kenntnisse sehr schwierig gewesen
- Bösartige HTTP-Requests werden nicht korrekt geschlossen
- Requests erscheinen nicht in den Server-Zugriffslogs
- Den meisten HTTP/2-Servern fehlen fortgeschrittene Funktionen zur Frame-Analyse
- Rohdaten der Verbindung müssen manuell analysiert werden
Koordinierte Offenlegung und Reaktion von CERT/CC
- Diese Schwachstellenfamilie stellte ein erhebliches Risiko für die Internetsicherheit dar
- Nach der Meldung im Januar 2024 eröffnete CERT/CC einen Vulnerability-Coordination-Case, um das Problem zu verfolgen
- Mehrere große Technologieunternehmen und Open-Source-Projekte beteiligten sich an dem Responsible-Disclosure-Prozess
- Da es für einen einzelnen Forscher schwierig ist, viele Implementierungen vollständig zu prüfen, war für ein Problem mit Auswirkungen auf mehrere Anbieter Vulnerability Coordination erforderlich
- CERT/CC veröffentlichte zu diesem Problem eine Vulnerability Note; solche Notes werden nur wenige Male pro Jahr veröffentlicht
1 Kommentare
Hacker-News-Kommentare
Wir haben genau dieses Problem letzten Monat in Bandit entschärft
https://github.com/mtrudel/bandit/blob/main/lib/bandit/http2...
Aus Sicht eines Implementierers ist das ehrlich gesagt etwas, das man ganz selbstverständlich abfangen muss. Ich hatte das schon lange auf dem Schirm und bin davon ausgegangen, dass andere Implementierungen sich natürlich ebenfalls dagegen schützen
Ich habe in den vergangenen Monaten Dutzende Implementierungen geprüft, und seltsamerweise hatten selbst große HTTP/2-Server diesen Schutz nicht oder falsch implementiert
Im Kern halte ich das für ein Ergebnis einer Entwicklerkultur, die daran gewöhnt ist, alles automatisch und dynamisch wachsen zu lassen, ohne sich darum zu kümmern, wie groß es werden kann
Diese Art Problem ist nicht auf HTTP/2 beschränkt, aber die enorme Komplexität von HTTP/2 hat vermutlich dazu beigetragen. Zu Zeiten von HTTP/1.x gab es mehr Entwickler, die mit Sprachen wie C vertraut waren und ständig auf Buffer-Längen geachtet haben; sie hätten Header-Allokationen über eine ganze Anfrage hinweg nicht unbegrenzt wachsen lassen, obwohl höchstens ein paar KB ausreichen
Viele Denial-of-Service-Angriffe wie slowloris oder Hash-Kollisionen bei Query-Parametern wurden erst dadurch realistisch, dass man begrenzte Ressourcennutzung erst nachträglich berücksichtigt hat
> Nicht betroffen: Nginx, Jetty, HAProxy, NetScaler, Varnish. [0]0: https://nowotarski.info/http2-continuation-flood/
Es wäre robuster gewesen, wenn zumindest der Vorschlag akzeptiert worden wäre, es nach nicht vollständig gefüllten HEADERS-Frames zu verbieten, aber man hielt das für etwas, das das Encoding selbst erschweren könnte. Etwa wegen Problemen wie Byte-Grenzen im Kompressor
Es ist amüsant, wie wir alle zehn Jahre dieselben Dinge „wiederentdecken“. Vor Kurzem war es die bekannte RESET_STREAM-Flood, diesmal ist es CONTINUATION, und bald kommen wohl DATA-Frames der Länge 0, WINDOW_UPDATE mit 1 Byte und CPU-intensive INITIAL_WINDOW-SETTINGS. Solange man bekannten Problemen nur einen Namen und nach Möglichkeit ein Logo geben kann, dreht die Welt diesen Sicherheitszirkus immer weiter
Ein früherer Beitrag desselben Autors, der die betroffenen Webserver/Reverse Proxys zusammenfasst
https://nowotarski.info/http2-continuation-flood/
Dieser Artikel war den ganzen Tag ganz oben
Ich frage mich: Wäre es für eine Website mit wenig Traffic vielleicht sicherer, sie einfach mit HTTP/1.1 zu betreiben?
HTTP/2 und HTTP/3 unterscheiden sich funktional deutlich. Durch Multiplexing, Windowing, HPACK usw. wird aus den nahezu zustandslosen Verbindungen von HTTP/1.1 eine zustandsbehaftete Verbindung. Um eine zustandsbehaftete Verbindung aufrechtzuerhalten, muss man Daten wie Zustand und Einstellungen speichern, und daraus entstehen solche Probleme
In HTTP/2 kommt Multiplexing hinzu, daher ändern sich auch die Abwehreigenschaften. Wenn eine Verbindung zum Beispiel von einer CDN-Origin-Anfrage stammt, kann man wenige Verbindungen zulassen und pro Verbindung einen großen Pool an Multiplexing-Kanälen haben; bei direktem Nutzerzugriff möchte man dagegen vielleicht viele Verbindungen zulassen, aber die Zahl der Multiplexing-Kanäle pro Verbindung verringern. Bei HTTP/1 sah fast alles ähnlich aus, deshalb war die Verteidigung deutlich einfacher
HTTP/1 hat viele schwer sichtbare Randbedingungen und altes Ausnahmeverhalten. Das Textformat ist viel flexibler, als es wirkt, wenn man nur gültige Header betrachtet, und es gibt auch mehrzeilige Header, alte MIME-Funktionen, Race Conditions bei 100-continue, benutzerdefinierte Hop-by-Hop-Header und vage Features wie GET-Bodys
Zum Glück dokumentieren die neuen HTTP-RFCs viele Fallstricke. Wer nur nach RFC 2616 implementiert, bekommt keine sichere Implementierung
Die tatsächliche Größe einer Anfrage oder Antwort kann auf mehrere Arten gleichzeitig angegeben werden, und die Werte können einander widersprechen. Außerdem hängt sie von verschiedenen Feature-Kombinationen und von Header-Werten ab, die aus Gründen der Abwärtskompatibilität seltsame Parsing-Regeln erfordern, sodass eine „einfache“ HTTP-Implementierung durch Request Smuggling ausgetrickst werden kann
In jedem Fall braucht man eine robuste, gut getestete und ausgereifte Implementierung
Beim üblichen Ausliefern statischer Assets besteht der Vorteil nur darin, dass HTTP/1 durch die Begrenzung der Verbindungen pro Domain limitiert ist und man daher mehr Assets parallel laden kann. Wenn man CDNs auf verschiedenen Domains verwendet, lässt sich auch dieses Problem meist umgehen
Theoretisch kann man mit HTTP/2 ungebündelte JavaScript-Assets ausliefern, aber in echten Produktionsumgebungen habe ich das nicht gesehen. Wahrscheinlich, weil in den meisten Fällen weiterhin ein Compile-Schritt nötig ist
Wenn man das langsam macht, könnte man es slowloris v2 nennen :(
HTTP/2, oder: wie man ein „Upgrade“ der Transportschicht in ein Protokoll der Anwendungsschicht hineinzwängt