Envoy HTTP/2 请求头大小限制绕过漏洞修复解析:被丢弃 Host 头的计量与 CVE-2026-73550
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
导读
本文基于 Envoy 官方变更日志 bug_fixes/http2__keep_size_of_dropped_host_header.rst 展开,深入解析 CVE-2026-73550(GHSA-qgf6-qvhw-4hvh)的修复原理:当 HTTP/2 请求中的Host头与:authority伪头重复而被丢弃时,其长度与数量此前未计入请求头 map 的大小与数量限制,从而形成可被利用的绕过窗口。读完本文,你将理解该漏洞的形成链路、修复在源码中的落点(codec_impl.cc 与 codec_impl.h)、对应的测试用例(codec_impl_test.cc),以及如何通过运行时开关回退这一行为变更。
背景:HTTP/2 中Host与:authority的去重逻辑
RFC 9113 的合规要求
HTTP/2 使用:authority伪头携带请求的目标主机信息,同时为兼容 HTTP/1.1 语义保留了传统Host头。RFC 9113 第 8.3.1 节规定:当请求同时携带:authority与Host时,二者必须一致,否则视为畸形请求;若二者一致,则属于冗余信息。
Enovy 从 1.27.4 起引入了运行时开关envoy.reloadable_features.http2_discard_host_header,其行为变更描述为:
Discard the
Hostheader if the:authorityheader was received to bring Envoy into compliance with RFC 9113 section 8.3.1.
该变更记录于 changelogs/1.27.4.yaml,实现位于服务端连接编解码器ServerConnectionImpl。
丢弃逻辑的源码位置
核心实现在 source/common/http/http2/codec_impl.cc 的ServerConnectionImpl::onHeader():
int ServerConnectionImpl::onHeader(int32_t stream_id, HeaderString&& name, HeaderString&& value) { if (http2_discard_host_header_) { StreamImpl* stream = getStreamUnchecked(stream_id); if (stream && name == static_cast(Http::Headers::get().HostLegacy)) { // Check if there is already the :authority header const auto result = stream->headers().get(Http::Headers::get().Host); if (!result.empty()) { // ... 丢弃 Host,但按新开关决定是否计量 } // Otherwise use host value as :authority } } return saveHeader(stream_id, std::move(name), std::move(value)); }其执行语义为:
- 开关
http2_discard_host_header_在ServerConnectionImpl构造函数中通过Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_discard_host_header")读取并锁定(codec_impl.cc); - 收到名为
host(即Http::Headers::get().HostLegacy)的头时,先检查当前流是否已有:authority映射出的 Host 值; - 若已有
:authority,则Host视为冗余并直接丢弃,不再进入saveHeader()。
丢弃带来的计量缺口
问题在于:被丢弃的Host头既没有存入stream->headers(),也没有被计数。而 Envoy 的 HTTP/2 请求头限制检查(总字节数max_request_headers_kb、总数量max_request_headers_count)只基于stream->headers()统计。于是攻击者可以不断塞入超大或海量的重复Host头——它们全部被静默丢弃、不计入限制,但实际占用的网络字节与解析开销真实存在,形成对头部大小限制的绕过窗口。这正是 CVE-2026-73550 的根因。
修复实现:对丢弃的 Host 头进行计量
新增的流级计数状态
修复在 source/common/http/http2/codec_impl.h 的StreamImpl中新增两个字段:
uint64_t discarded_host_header_size_{0}; uint32_t discarded_host_header_count_{0};discarded_host_header_size_:累加被丢弃 Host 头的字节数(name 长度 + value 长度);discarded_host_header_count_:累加被丢弃 Host 头的个数。
丢弃路径上的计量逻辑
onHeader()中在确定要丢弃Host头时,先判断新开关envoy.reloadable_features.http2_track_size_of_dropped_host_header(该开关在 source/common/runtime/runtime_features.cc 以RUNTIME_GUARD声明):
if (!result.empty()) { if (Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.http2_track_size_of_dropped_host_header")) { // Discard the host header value but track its size for enforcing received header map // limits. stream->discarded_host_header_size_ += name.size() + value.size(); stream->discarded_host_header_count_++; stream->bytes_meter_->addDecompressedHeaderBytesReceived(name.size() + value.size()); return checkHeaderLimits(*stream); } else { return 0; } }三个动作值得逐一说明:
- 字节与数量累加:
name.size() + value.size()同时计入头部名与头部值的长度,与普通头部的计量口径一致; - 字节计量器同步:调用
bytes_meter_->addDecompressedHeaderBytesReceived(...),保证流量/字节统计(如 access log 与流量计费相关统计)不因丢弃而失真; - 立即触发限制检查:直接返回
checkHeaderLimits(*stream),而不是静默返回 0,从而让超大/超量的丢弃 Host 头同样触发限制。
限制检查的统一口径
checkHeaderLimits()(codec_impl.cc)现在把丢弃头纳入统计:
int ConnectionImpl::checkHeaderLimits(StreamImpl& stream) { uint64_t headers_size = stream.headers().byteSize() + stream.discarded_host_header_size_; uint64_t headers_count = stream.headers().size() + stream.discarded_host_header_count_; if (http2_include_cookies_in_limits_) { headers_size += stream.cookies_.size(); headers_count += stream.cookie_count_; } if (headers_size > max_headers_kb_ * 1024) { stream.setDetails(Http2ResponseCodeDetails::get().header_list_size_too_large); stats_.header_list_size_too_large_.inc(); return ERR_TEMPORAL_CALLBACK_FAILURE; } // ... count 超限走 stats_.header_overflow_.inc() }headers_size与headers_count的计算统一为「已保存头部 + 被丢弃 Host 头 + 可选 Cookie」。同一口径还被复用于直方图统计recordHistogramsForStream()(codec_impl.cc),确保header_list_size_、header_count_等直方图数据也反映真实接收的头部规模,不因丢弃而低估。
触发保护时的行为与相关统计
限制超限后的处理
当累计后的headers_size > max_headers_kb_ * 1024时,流会:
- 设置错误详情
http2.header_list_size_too_large(定义于 codec_impl.cc); - 递增计数器
stats_.header_list_size_too_large_.inc(); - 返回
ERR_TEMPORAL_CALLBACK_FAILURE,由上层触发流重置(RST_STREAM)。
数量超限(headers_count超过max_request_headers_count)则递增http2.header_overflow计数器。这两个统计分别对应常规头部限制与丢弃头计数超限两类场景。
关键统计与配置参数
| 参数/统计 | 含义 | 源码位置 |
|---|---|---|
max_request_headers_kb | 请求头总字节上限(KB),默认 60KB | codec_impl.cc |
max_request_headers_count | 请求头数量上限 | codec_impl.cc |
http2.header_list_size_too_large | 头部总大小超限计数 | codec_impl.cc |
http2.header_overflow | 头部数量超限计数 | codec_impl.cc |
discarded_host_header_size_/discarded_host_header_count_ | 流级丢弃 Host 计量 | codec_impl.h |
测试用例如何验证修复
测试文件 test/common/http/http2/codec_impl_test.cc 提供了正反两组用例,直观展示修复前后的行为差异。
用例一:修复生效(默认开启)时触发数量限制
TooManyHeadersWithDiscardedHostHeader(codec_impl_test.cc)构造了 4 个伪头(:method、:scheme、:path、:authority)外加一个host: two.example.com,并将max_request_headers_count_设为 4:
- 请求总头数为 5,超过 4 的限制;
- 断言
server_stream_callbacks_.onResetStream(...)与onCodecLowLevelReset()被调用(流被重置); - 断言
http2.header_overflow计数为 1。
该用例证明:默认开启计量后,被丢弃的Host头也会把计数推向超限,从而触发流重置。
用例二:回退开关关闭时不再触发
HeaderListSizeTooLargeWithDiscardedHostHeaderAllowedWithOverride(codec_impl_test.cc)通过TestScopedRuntime显式设置:
scoped_runtime.mergeValues( {{"envoy.reloadable_features.http2_track_size_of_dropped_host_header", "false"}});随后构造large_host(2000 字节'a')作为 Host 头,max_request_headers_kb_仅 2:
- 断言
decodeHeaders_正常完成; - 断言
onResetStream与onCodecLowLevelReset不被调用(Times(0)); - 断言
http2.header_list_size_too_large计数为 0。
该用例证实:回退后丢弃 Host 头不计入大小限制,2000 字节的 Host 头可安然绕过 2KB 的头部限制——这正是漏洞的原始形态,也即运维者回退开关时必须知晓的风险。同文件的TooManyHeadersWithDiscardedHostHeaderAllowedWithOverride(codec_impl_test.cc)在数量维度验证了同样的回退行为。
注意:
HeaderListSizeTooLarge...Override用例在http2_implementation_ == Http2Impl::Oghttp2时跳过(GTEST_SKIP),因为 Oghttp2 实现内部自带头部大小限制检查,行为与 nghttp2 路径不同,测试注释对此有明确说明。
运行时开关与回退操作
开关声明位置
两个相关开关都在 source/common/runtime/runtime_features.cc 中以RUNTIME_GUARD声明:
RUNTIME_GUARD(envoy_reloadable_features_http2_discard_host_header); // L86 RUNTIME_GUARD(envoy_reloadable_features_http2_track_size_of_dropped_host_header); // L90回退方式
修复本身引入了行为变更,变更日志(changelogs/current/bug_fixes/http2__keep_size_of_dropped_host_header.rst 与 changelogs/1.38.4.yaml)明确说明:
This behavioral change can be reverted by setting the runtime guard
envoy.reloadable_features.http2_track_size_of_dropped_host_headertofalse.
即通过 runtime 配置(bootstrap 的runtime_layer或 runtime 文件)设置:
runtime: overrides: envoy.reloadable_features.http2_track_size_of_dropped_host_header: "false"即可恢复旧行为:丢弃 Host 头不参与大小/数量限制计算。但回退等于重新暴露 CVE-2026-73550 的绕过面,仅在兼容性排查等必要场景下临时使用,并在确认无异常后尽快恢复默认值(开启)。
开关的锁定语义
需要注意http2_discard_host_header属于热点路径(hot-path)运行时特性。仓库中 changelogs/current/minor_behavior_changes/http__latch-hot-path-runtime-flags.rst 说明:这类运行时特性现在在首次访问时锁定(latched),即连接建立后不再感知 runtime 的后续变更。因此http2_track_size_of_dropped_host_header的开关变更需要在 Envoy 进程重启(或至少在新的连接建立前)生效,对存量长连接不会热生效,运维调整时请预留连接收敛窗口。
从源码结构看该修复的设计要点
- 计量与丢弃解耦:丢弃 Host 的逻辑(
http2_discard_host_header,RFC 9113 合规)与计量逻辑(http2_track_size_of_dropped_host_header,安全加固)是两个独立开关,分别可回退,便于分阶段灰度; - 统一限制口径:
checkHeaderLimits()与recordHistogramsForStream()共用「已保存 + 丢弃 + Cookie」的累计口径,避免统计口径分裂导致二次绕过; - 字节计量器同步:在丢弃路径上同步
bytes_meter_,保证即使头部被丢弃,真实接收字节仍进入流量统计,维持可观测性; - 测试正反双证:默认开启触发限制、显式回退后不触发限制,两组测试从正反两面锁定了修复行为,防止回归。
版本与影响范围
- 本次修复对应的安全通告为 CVE-2026-73550(GHSA-qgf6-qvhw-4hvh),记录于 changelogs/current/bug_fixes/http2__keep_size_of_dropped_host_header.rst;
- 相同修复同时被收录进 1.38.4(changelogs/1.38.4.yaml)、1.37.6(changelogs/1.37.6.yaml)、1.39.1(changelogs/1.39.1.yaml)与 1.36.10(changelogs/1.36.10.yaml)等多个维护分支的变更日志中,覆盖了当前支持的稳定版本线;
- 修复适用于所有启用了 HTTP/2 服务端编解码(
ServerConnectionImpl)的下行接入场景,凡接收 HTTP/2 请求头的监听器均受影响。
总结
CVE-2026-73550 是 HTTP/2 头部去重与头部限制计量口径不一致导致的绕过漏洞:Host头在匹配:authority后被丢弃,却未计入请求头 map 的大小与数量限制。修复通过StreamImpl新增discarded_host_header_size_/discarded_host_header_count_两个字段,在丢弃路径上同步计量并复用checkHeaderLimits()统一检查,同时配套正反两组测试用例与可回退运行时开关。对运维者而言,升级到包含该修复的版本即可消除绕过面;如确需临时回退,务必理解其重新暴露的风险,并留意热点路径运行时开关的锁定语义。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考