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 已经会以 ProtocolError 拒绝重复的 Content-Length 头。同样的修复从未应用于 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 值,并且只检查:
:authority 或 Host 中至少有一个存在没有对 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 - 隐蔽::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 - 单个不匹配的 Host 与 :authority(被正确拒绝)
[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) 发现并报告