Envoy 安全修复深度解析:泛化 HTTP Upgrade 请求体暂停(CVE-2026-73548)
Envoy 安全修复深度解析:泛化 HTTP Upgrade 请求体暂停(CVE-2026-73548)
导读
本文围绕 Envoy 仓库中一条安全修复变更记录(changelogs/current/bug_fixes/http__pause-generic-upgrade-request-body.rst)展开,深入剖析其对应的 CVE-2026-73548(GHSA-3vhp-c83q-jqc2)漏洞成因、修复原理与运行时开关。你将理解:为什么泛化 Upgrade 请求在握手前发送的 payload 会被上游误解析为 HTTP/1 管线化请求,进而“污染”共享的上游连接;以及 Envoy 如何通过暂停泛化升级请求体来消除这一风险,并掌握通过 envoy.reloadable_features.http_pause_generic_upgrade_request_body 运行时标志进行临时回退的方法。
一、安全通告原文与漏洞概述
该 .rst 变更记录全文如下:
Fix: CVE-2026-73548。 Fixed a vulnerability where payload sent before a generic HTTP upgrade was accepted could be interpreted as a pipelined HTTP/1 request and poison a shared upstream connection. Generic upgrade payload is now paused until the upstream accepts the upgrade. This change can be temporarily reverted by setting
envoy.reloadable_features.http_pause_generic_upgrade_request_bodytofalse.
从中可以提炼出三个核心信息:
- 漏洞编号:CVE-2026-73548,对应安全通告 GHSA-3vhp-c83q-jqc2;
- 漏洞本质:在泛化(非 WebSocket)HTTP Upgrade 被上游接受之前,客户端先行发送的 payload 可能被上游解释为一条管线化(pipelined)的 HTTP/1 请求,从而污染共享的上游连接;
- 修复方式:泛化 Upgrade 的 payload 现在会被“暂停”,直到上游接受 Upgrade 为止;该行为可通过运行时标志临时回退到旧行为。
二、漏洞成因:连接共享与 Upgrade 预载荷的碰撞
要理解该漏洞,需要先厘清三个背景机制。
1. HTTP Upgrade 与泛化 Upgrade
在 HTTP/1 中,客户端通过 Connection: upgrade 与 Upgrade: <protocol> 头请求将当前连接切换到其它协议(如 WebSocket)。Envoy 对 Upgrade 的处理并不局限于 WebSocket——任何带 Upgrade 头的请求都可配置为泛化升级(generic upgrade)。
在 source/common/http/utility.cc 中可看到判定逻辑:
bool Utility::isUpgrade(const RequestOrResponseHeaderMap& headers) {
// 例如 Firefox 的 Connection 头是 "keep-alive, Upgrade",需用 token 匹配
return (headers.Upgrade() &&
Envoy::StringUtil::caseFindToken(headers.getConnectionValue(), ",",
Http::Headers::get().ConnectionValues.Upgrade));
}
bool Utility::isWebSocketUpgradeRequest(const RequestHeaderMap& headers) {
return (isUpgrade(headers) &&
absl::EqualsIgnoreCase(headers.getUpgradeValue(),
Http::Headers::get().UpgradeValues.WebSocket));
}
凡是满足 isUpgrade 但 Upgrade 值不是 websocket 的,即属于“泛化 Upgrade”。
2. HTTP/1 管线化与连接复用
HTTP/1 上游连接是共享的(keep-alive + 连接池)。正常情况下,一个请求结束后连接会归还连接池供后续请求复用。如果连接上残留了未被消费的字节,这些字节会被下一个复用该连接的请求当作其“请求头”的一部分来解析——这正是“连接污染(poison a shared upstream connection)”的由来。
3. 漏洞触发路径
攻击场景为 HTTP/2 下行 → HTTP/1 上行的组合(对应测试中的 downstreamProtocol() == Http2 && upstreamProtocol() == Http1):
- 客户端发起一个泛化 Upgrade 请求(例如
Upgrade: foo),并在握手前就随请求发送数据(即“pre-payload”); - 修复前,Envoy 会把该 payload 原样转发给上游 HTTP/1 连接;
- 若上游拒绝 Upgrade(例如返回
200 OK),这条 HTTP/1 连接上已写入的 payload 并未被消费,连接返回连接池; - 后续其他请求复用了这条“被污染”的连接,其 payload 会被上游当作新请求的第一行解析——即发生请求走私(request smuggling),攻击者可能借此注入任意请求。
测试用例 NonWebsocketUpgradeWithPrePayloadDoesNotPoisonConnection(位于 test/integration/websocket_integration_test.cc)精确复现了该攻击:发送 GET /smuggled HTTP/1.1\r\nHost: sni.lyft.com\r\n\r\n 作为预载荷,随后验证“受害者”请求 /victim 使用的是全新连接,且上游没有收到被注入的 /smuggled 请求。
三、修复实现:UpstreamRequest 的三类暂停位
修复的核心位于 HTTP 路由的上游请求管理类 UpstreamRequest。在 source/common/router/upstream_request.h 中定义了三类暂停标志:
// True if the CONNECT headers have been sent but proxying payload is paused
// waiting for response headers.
bool paused_for_connect_ : 1 = false;
bool paused_for_websocket_ : 1 = false;
// True if generic (non-WebSocket) upgrade headers have been sent but proxying payload is
// paused waiting for the upstream to accept the upgrade.
bool paused_for_generic_upgrade_ : 1 = false;
可见 Envoy 在此之前已经为 CONNECT 与 WebSocket 握手实现了“等上游响应再转发 payload”的暂停机制,本次修复将同样的保护扩展到了泛化 Upgrade,三者形成统一的语义。
触发暂停:收到请求头时
在 source/common/router/upstream_request.cc 的 acceptHeadersFromRouter 中,按优先级设置暂停标志:
auto* headers = parent_.downstreamHeaders();
if (headers->getMethodValue() == Http::Headers::get().MethodValues.Connect) {
paused_for_connect_ = true;
// WebSocket upgrade 请求暂停,直到上游返回 101 Switching Protocols
} else if (Http::Utility::isWebSocketUpgradeRequest(*headers)) {
paused_for_websocket_ = true;
...
} else if (!end_stream &&
Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.http_pause_generic_upgrade_request_body") &&
Http::Utility::isUpgrade(*headers)) {
// 暂停泛化(非 WebSocket)Upgrade 的 payload 转发,直到上游接受 Upgrade
paused_for_generic_upgrade_ = true;
}
值得注意的细节:
paused_for_generic_upgrade_的判定要求!end_stream,即请求头之后确实还有请求体数据需要暂停时才设置;- 该分支受运行时标志
envoy.reloadable_features.http_pause_generic_upgrade_request_body门控,这正是安全通告中提到的回退开关; - 暂停逻辑在
conn_pool_->newStream(this)之前设置完成,因此新流创建、连接池回调等流程能感知到暂停状态。
执行暂停:过滤链末端停止迭代
暂停的实际执行者是连接池回调 onPoolReady 与请求过滤器链。在 source/common/router/upstream_codec_filter.cc 中,UpstreamCodecFilter 作为过滤器链的最后一环,一旦检测到任一暂停标志,就返回 StopAllIterationAndWatermark,将后续数据(请求体)留在缓冲区内不再向编解码器(codec)投递:
if (callbacks_->upstreamCallbacks()->pausedForConnect()) {
return Http::FilterHeadersStatus::StopAllIterationAndWatermark;
} else if (callbacks_->upstreamCallbacks()->pausedForWebsocketUpgrade()) {
return Http::FilterHeadersStatus::StopAllIterationAndWatermark;
} else if (callbacks_->upstreamCallbacks()->pausedForGenericUpgrade()) {
return Http::FilterHeadersStatus::StopAllIterationAndWatermark;
}
return Http::FilterHeadersStatus::Continue;
StopAllIterationAndWatermark 同时会激活下游水印流控:当缓冲区水位超过高水位时,Envoy 会暂停从下游读取,从而形成对下游发送方的背压,避免无界缓冲。
解除暂停:上游接受 Upgrade 时
在 source/common/router/upstream_codec_filter.cc,当上游响应头到达时,依据状态码判断 Upgrade 是否被接受:
if (filter_.callbacks_->upstreamCallbacks()->pausedForGenericUpgrade()) {
const uint64_t status = Http::Utility::getResponseStatus(*headers);
const auto protocol = filter_.callbacks_->upstreamCallbacks()->upstreamStreamInfo().protocol();
if (status == static_cast<uint64_t>(Http::Code::SwitchingProtocols) ||
(protocol.has_value() && protocol.value() != Envoy::Http::Protocol::Http11 &&
Http::CodeUtility::is2xx(status))) {
filter_.callbacks_->upstreamCallbacks()->setPausedForGenericUpgrade(false);
filter_.callbacks_->continueDecoding();
}
}
解除条件为二选一:
- 上游返回 101 Switching Protocols(HTTP/1 的标准握手成功响应);
- 上游为 HTTP/2 / HTTP/3 且返回 2xx(HTTP/2 没有 101,用 2xx 表示 Upgrade 成功)。
continueDecoding() 会恢复过滤器链迭代,将此前缓冲的请求体转发给上游。反之,若上游拒绝 Upgrade(如 200/4xx),请求体不会被转发,连接随后被关闭,也就不会回到连接池供他人复用。
另外在 source/common/router/upstream_request.cc 中,onPoolReady 对非 HTTP 上游(如 CONNECT 终结的 TCP/UDP)会直接清空所有暂停标志,确保非 HTTP 场景不受影响。
四、运行时开关:如何回退与配置
修复通过 Envoy 标准的 reloadable feature 运行时门控实现,开关定义于 source/common/runtime/runtime_features.cc:
RUNTIME_GUARD(envoy_reloadable_features_http_pause_generic_upgrade_request_body);
- 默认值:
true(新行为,暂停泛化 Upgrade 请求体); - 回退方法:在运行时配置中将
envoy.reloadable_features.http_pause_generic_upgrade_request_body设为false,即可临时恢复旧行为(不暂停、直接转发)。
典型运行时配置(--runtime-files 指向的 YAML,或控制面下发的 runtime layer)示意:
runtime:
- name: main
layers:
- name: static_layer_0
static_layer:
envoy:
reloadable_features:
http_pause_generic_upgrade_request_body: "false"
注意:回退仅为应急手段。关闭该开关意味着重新暴露请求走私风险,建议仅在需要临时兼容旧客户端/上游行为时使用,并尽快恢复默认开启。
五、测试验证:从复现到回归
仓库在 test/integration/websocket_integration_test.cc 中新增了三个针对性测试(均限定 HTTP/2 下行 → HTTP/1 上行 组合,且通过 TestScopedRuntime 显式设置开关值):
| 测试 | 开关值 | 验证点 |
|---|---|---|
NonWebsocketUpgradeWithPrePayloadDoesNotPoisonConnection(L319-L391) |
true |
预载荷不被转发到上游;上游拒绝后连接被关闭,受害者请求使用全新连接,无 /smuggled 注入 |
NonWebsocketUpgradeBuffersPrePayloadUntilAccepted(L393-L428) |
true |
预载荷 hello 在上游返回 101 前被缓冲,握手成功后才转发 |
NonWebsocketUpgradePrePayloadRuntimeGuardDisabled(L430+) |
false |
开关关闭时恢复旧行为,预载荷立即转发到上游 |
三个测试从正反两面覆盖了修复语义:开关开启时“先握手、后传数据”,且失败时连接不回流;开关关闭时保持旧行为以便对照。udp_tunneling_integration_test.cc(L123)中同样将该开关置为 true,说明 CONNECT-UDP 等场景也纳入同一保护框架。
六、修复原理总结与运维建议
| 环节 | 修复前 | 修复后 |
|---|---|---|
| 泛化 Upgrade 请求体 | 立即转发给上游 HTTP/1 连接 | 缓冲暂停,等待上游接受 Upgrade |
| 上游拒绝 Upgrade | payload 残留在连接上,连接回流连接池 | payload 不转发,连接关闭不回流 |
| 风险 | 请求走私(payload 被当作管线化请求) | 消除走私面,代价是缓冲区占用 |
| 开关 | — | envoy.reloadable_features.http_pause_generic_upgrade_request_body |
给运维与开发者的落地建议:
- 确认版本:升级到包含该修复(默认开启)的 Envoy 版本;
- 默认保持开启:该行为受 reloadable feature 门控,默认
true,无需额外配置; - 关注缓冲区水位:暂停机制会占用请求体缓冲区,若部署了长连接+大 payload 的泛化 Upgrade 场景,可结合下游水印配置(
max_request_bytes与流控)评估内存影响; - 应急回退:仅在必要时通过运行时开关设为
false临时恢复旧行为,并跟踪 CVE-2026-73548 / GHSA-3vhp-c83q-jqc2 的后续通告; - 审计升级配置:泛化 Upgrade 需在 HttpConnectionManager 的
upgrade_configs中显式声明升级类型(测试中使用hcm.add_upgrade_configs()->set_upgrade_type("foo")),未声明的 Upgrade 行为不受本修复影响,请一并核对。
延伸阅读
- 变更记录原文:changelogs/current/bug_fixes/http__pause-generic-upgrade-request-body.rst
- 核心实现:source/common/router/upstream_request.cc、source/common/router/upstream_codec_filter.cc
- 暂停标志定义:source/common/router/upstream_request.h
- 运行时开关注册:source/common/runtime/runtime_features.cc
- 集成测试:test/integration/websocket_integration_test.cc