Envoy HTTP/2 请求头大小限制绕过漏洞修复解析:被丢弃 Host 头的计量与 CVE-2026-73550

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 theHostheader 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)); }

其执行语义为:

  1. 开关http2_discard_host_header_在ServerConnectionImpl构造函数中通过Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_discard_host_header")读取并锁定(codec_impl.cc);
  2. 收到名为host(即Http::Headers::get().HostLegacy)的头时,先检查当前流是否已有:authority映射出的 Host 值;
  3. 若已有: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; } }

三个动作值得逐一说明:

  1. 字节与数量累加:name.size() + value.size()同时计入头部名与头部值的长度,与普通头部的计量口径一致;
  2. 字节计量器同步:调用bytes_meter_->addDecompressedHeaderBytesReceived(...),保证流量/字节统计(如 access log 与流量计费相关统计)不因丢弃而失真;
  3. 立即触发限制检查:直接返回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),默认 60KBcodec_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 guardenvoy.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 进程重启(或至少在新的连接建立前)生效,对存量长连接不会热生效,运维调整时请预留连接收敛窗口。

从源码结构看该修复的设计要点

  1. 计量与丢弃解耦:丢弃 Host 的逻辑(http2_discard_host_header,RFC 9113 合规)与计量逻辑(http2_track_size_of_dropped_host_header,安全加固)是两个独立开关,分别可回退,便于分阶段灰度;
  2. 统一限制口径:checkHeaderLimits()与recordHistogramsForStream()共用「已保存 + 丢弃 + Cookie」的累计口径,避免统计口径分裂导致二次绕过;
  3. 字节计量器同步:在丢弃路径上同步bytes_meter_,保证即使头部被丢弃,真实接收字节仍进入流量统计,维持可观测性;
  4. 测试正反双证:默认开启触发限制、显式回退后不触发限制,两组测试从正反两面锁定了修复行为,防止回归。

版本与影响范围

  • 本次修复对应的安全通告为 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),仅供参考