Envoy 安全修复深度解析:泛化 HTTP Upgrade 请求体暂停(CVE-2026-73548)

原创2026-09-11 19:13:471,666 阅读
文章标签:云原生服务网格网络微服务

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_body to false.

从中可以提炼出三个核心信息:

  1. 漏洞编号:CVE-2026-73548,对应安全通告 GHSA-3vhp-c83q-jqc2;
  2. 漏洞本质:在泛化(非 WebSocket)HTTP Upgrade 被上游接受之前,客户端先行发送的 payload 可能被上游解释为一条管线化(pipelined)的 HTTP/1 请求,从而污染共享的上游连接;
  3. 修复方式:泛化 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):

  1. 客户端发起一个泛化 Upgrade 请求(例如 Upgrade: foo),并在握手前就随请求发送数据(即“pre-payload”);
  2. 修复前,Envoy 会把该 payload 原样转发给上游 HTTP/1 连接;
  3. 若上游拒绝 Upgrade(例如返回 200 OK),这条 HTTP/1 连接上已写入的 payload 并未被消费,连接返回连接池;
  4. 后续其他请求复用了这条“被污染”的连接,其 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

给运维与开发者的落地建议:

  1. 确认版本:升级到包含该修复(默认开启)的 Envoy 版本;
  2. 默认保持开启:该行为受 reloadable feature 门控,默认 true,无需额外配置;
  3. 关注缓冲区水位:暂停机制会占用请求体缓冲区,若部署了长连接+大 payload 的泛化 Upgrade 场景,可结合下游水印配置(max_request_bytes 与流控)评估内存影响;
  4. 应急回退:仅在必要时通过运行时开关设为 false 临时恢复旧行为,并跟踪 CVE-2026-73548 / GHSA-3vhp-c83q-jqc2 的后续通告;
  5. 审计升级配置:泛化 Upgrade 需在 HttpConnectionManager 的 upgrade_configs 中显式声明升级类型(测试中使用 hcm.add_upgrade_configs()->set_upgrade_type("foo")),未声明的 Upgrade 行为不受本修复影响,请一并核对。

延伸阅读

登录后查看全文
envoy