Envoy ext_authz 过滤器的 UAF 修复详解:HTTP 鉴权拒绝请求路径上的内存安全修复
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
导读
本文基于 Envoy 仓库中 changelogs/current/bug_fixes/ext_authz__fix_uaf_when_rejecting_request.rst 记录的漏洞修复(CVE-2026-50572)展开,深入剖析 ext_authz(External Authorization,外部鉴权)过滤器在通过 HTTP 协议调用外部鉴权服务并拒绝请求时存在的 use-after-free(UAF,释放后使用)内存安全问题及其修复。读完本文,你将掌握 ext_authz 过滤器的完整请求处理链路、UAF 漏洞的成因与触发场景,以及 Envoy 如何通过生命周期管理和引用计数机制从根本上消除此类风险。
一、安全公告内容与影响面
根据 changelogs/current/bug_fixes/ext_authz__fix_uaf_when_rejecting_request.rst 的官方记录,本次修复针对的是安全公告CVE-2026-50572(对应 GitHub Advisory GHSA-q8wp-gf7q-m8cv),问题定性为:
Fixed UAF when ext_authz over HTTP causes request to be rejected. (修复了 ext_authz 通过 HTTP 调用鉴权服务并拒绝请求时产生的释放后使用问题。)
这条变更记录言简意赅,却指向一个安全敏感的领域:HTTP 传输模式下的 ext_authz 过滤器。该过滤器本身承担着网关的访问控制职责,一旦在"拒绝(deny)"路径上出现 UAF,攻击者可能借机触发崩溃(拒绝服务)甚至进一步的内存破坏。
同目录下还有一条姊妹修复 changelogs/current/bug_fixes/ext_authz__fix-handling-of-pathless-requests.rst(CVE-2026-73547),修复的是无 URI 路径请求(如 CONNECT)导致进程异常终止的问题,两者共同构成近期 ext_authz 过滤器在 HTTP 模式下的安全性加固。
二、背景:ext_authz 过滤器在 HTTP 模式下的工作流
2.1 过滤器在 Envoy 中的定位
ext_authz(External Authorization)是 Envoy 内置的核心 HTTP 过滤器,允许 Envoy 在将请求转发到上游之前,先调用一个外部鉴权服务进行校验。它支持两种传输协议:
- gRPC 模式:使用
envoy.service.auth.v3.AuthorizationgRPC 服务,通过CheckRPC 完成鉴权; - HTTP 模式(本文主角):通过普通的 HTTP
POST请求将原始请求属性(方法、路径、头部等)发送给鉴权服务,依据 HTTP 响应状态码判断允许(200)或拒绝(其余状态码)。
过滤器的核心实现位于 source/extensions/filters/http/ext_authz/ext_authz.cc,对应的类声明在 source/extensions/filters/http/ext_authz/ext_authz.h。
2.2 请求处理主链路:decode → check → onComplete
从 ext_authz.cc 可以看到,过滤器在decodeHeaders()中发起对外部鉴权服务的调用:
Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) { ... // Initiate a call to the authorization server since we are not disabled. initiateCall(headers); return filter_return_ == FilterReturn::StopDecoding ? Http::FilterHeadersStatus::StopAllIterationAndWatermark : Http::FilterHeadersStatus::Continue; }initiateCall()(ext_authz.cc#L321-L442)会完成 CheckRequest 的组装(头部、查询参数、元数据等),然后调用客户端的check()方法,并把过滤器自身作为RequestCallbacks传入:
state_ = State::Calling; filter_return_ = FilterReturn::StopDecoding; // 暂停过滤器链继续迭代 active_client_ = client_to_use; initiating_call_ = true; client_to_use->check(*this, check_request_, decoder_callbacks_->activeSpan(), decoder_callbacks_->streamInfo()); initiating_call_ = false;这里的关键是active_client_成员(ext_authz.h#L566 处声明为Filters::Common::ExtAuthz::Client* active_client_{nullptr}),它持有的是当前正在服务这一次 in-flight 鉴权请求的客户端裸指针(raw pointer)。这个裸指针正是本次 UAF 风险的核心关注点。
当鉴权服务返回后,HTTP 客户端实现会回调过滤器的onComplete()(ext_authz.cc#L700-L1160)。以"拒绝"分支为例(ext_authz.cc#L1012-L1094):
case CheckStatus::Denied: { ... // setResponseFlag must be called before sendLocalReply decoder_callbacks_->streamInfo().setResponseFlag( StreamInfo::CoreResponseFlag::UnauthorizedExternalService); decoder_callbacks_->sendLocalReply( response->status_code, response->body, &headers = response->headers_to_set, &callbacks = *decoder_callbacks_, this -> void { ... }, std::nullopt, Filters::Common::ExtAuthz::ResponseCodeDetails::get().AuthzDenied); break; }sendLocalReply()会直接向下游返回鉴权拒绝的响应(例如 403 Forbidden),随后过滤器链被终止、流被销毁。
三、UAF 漏洞成因剖析:HTTP 客户端对象生命周期与裸指针
3.1 引用:active_client_裸指针的保存与清空
在initiateCall()中,active_client_ = client_to_use被赋值为当前调用使用的客户端对象指针;随后在onComplete()的开头(ext_authz.cc#L705-L706):
updateLoggingInfo(response->grpc_status); active_client_ = nullptr;以及在onDestroy()(ext_authz.cc#L679-L687)中:
void Filter::onDestroy() { if (state_ == State::Calling) { state_ = State::Complete; if (active_client_ != nullptr) { active_client_->cancel(); active_client_ = nullptr; } } }3.2 UAF 的触发场景(修复前)
从代码结构上可以推断,修复前的问题出在客户端对象(RawHttpClientImpl)的销毁时机上:
- HTTP 模式下的客户端是每流创建的。与 gRPC 模式下复用全局异步客户端不同,HTTP 模式的
RawHttpClientImpl是伴随当前流创建、由流自身持有生命周期的; - 当鉴权服务返回"拒绝",过滤器通过
sendLocalReply()立即终止了流。此时流的销毁顺序可能导致:HTTP 客户端对象先于过滤器回调完成对响应数据的解析而被释放; - 一旦客户端对象被释放,而
onComplete()回调路径中仍存在对该客户端对象(例如其内部的 stream info、上游字节计量器bytes_meter等)的访问,就会形成典型的UAF:悬空指针被解引用。
特别地,updateLoggingInfo()(ext_authz.cc#L637-L673)中存在对该指针所指向对象的深入访问:
// Use the client that actually served this check request for stream info. auto const* stream_info = active_client_ ? active_client_->streamInfo() : client_->streamInfo(); if (stream_info == nullptr) { return; } const auto& bytes_meter = stream_info->getUpstreamBytesMeter(); ...如果active_client_指向的对象已被释放,active_client_->streamInfo()就是对悬空指针的访问,属于典型的释放后使用。同理,在拒绝路径上,response->headers_to_set等响应对象如果与客户端内部缓冲的生命周期纠缠在一起,也可能在流销毁后仍被回调访问。
3.3 为什么 HTTP 模式特别容易触发
对比两条传输通道的实现:
- gRPC 客户端由
grpcAsyncClientManager统一管理,以 shared_ptr 形式引用,生命周期与集群强绑定,流销毁不会导致客户端消失; - HTTP 客户端(
RawHttpClientImpl,见 source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc)是按流创建、随流销毁的临时对象。从 ext_authz.cc#L298-L319 的createPerRouteHttpClient()可以看到,每个流都会std::make_unique<RawHttpClientImpl>(...)。
因此,HTTP 模式下的客户端生命周期与流的销毁时序耦合极深,一旦"拒绝 + 立即终止流"的路径执行顺序有偏差,客户端被提前释放的风险远高于 gRPC 模式。这也解释了为何该 CVE 明确限定于 "ext_authz over HTTP"。
四、修复方案的工程解读
从当前仓库代码可以观察到,围绕该 UAF 已经形成了一套完整的生命周期防护体系:
- 显式状态机管理:过滤器通过
State { NotStarted, Calling, Complete }枚举(ext_authz.h#L553)跟踪与鉴权服务的通信阶段。onDestroy()中仅在State::Calling时执行取消逻辑,避免在回调已经完成(Complete)后再次触碰客户端; - 回调入口处立即解绑:
onComplete()一进入就将active_client_置空,确保后续任何对客户端对象的访问都发生在解绑之前,杜绝"回调执行一半、客户端已释放"的窗口; - 流销毁时的取消协议:
onDestroy()中调用active_client_->cancel()并置空指针,保证在流被终止(包括被拒绝后终止)时,挂起的异步回调不会再去触碰已销毁的客户端; - 空指针防御:
updateLoggingInfo()中对active_client_与stream_info都做了判空,即使客户端不可用,也不会因悬空指针访问而崩溃。
测试方面,test/extensions/filters/http/ext_authz/ext_authz_filter_test.cc 中大量用例覆盖了拒绝路径的sendLocalReply期望(例如 1754、1793、1831、3206、3252 等行的EXPECT_CALL(decoder_filter_callbacks_, sendLocalReply(...))),以及 3927-3929 行展示的"先 cancel 再 onDestroy"的销毁协议测试,从单元测试层面锁定了客户端生命周期的正确管理。这也印证了 test/extensions/filters/http/ext_authz/ext_authz_filter_test.cc#L3927-L3929 中取消与销毁的顺序要求。
五、拒绝路径的完整生命周期时序
结合源码,修复后"HTTP 鉴权拒绝请求"的完整时序可归纳为:
decodeHeaders()收到客户端请求,过滤器组装CheckRequest并发起 HTTP 调用(initiateCall()),记录active_client_,将filter_return_置为StopDecoding;- HTTP 鉴权服务返回非 200 状态码,
RawHttpClientImpl解析响应并回调onComplete(); onComplete()立即将active_client_置空并更新日志信息(此刻客户端仍存活);- 进入
CheckStatus::Denied分支:校验响应头、按max_denied_response_body_bytes截断响应体、设置UnauthorizedExternalService响应标志位,最后通过sendLocalReply()向下游返回拒绝响应(AuthzDenied响应码详情); - 流进入终止流程,过滤器链调用
onDestroy():此时state_已是Complete,不再执行取消操作,避免二次触碰已回收的客户端对象。
上述第 3 步与第 5 步的配合,正是本次修复消除 UAF 的关键:回调完成即解绑,流销毁不再干预已完成的状态。
六、运维与升级建议
- 升级路径:修复包含在 CVE-2026-50572 对应的安全版本中,所有在生产环境使用HTTP 模式 ext_authz的部署都应在回归测试后尽快升级;
- 风险评估:本次漏洞仅影响
ext_authz配置为http_service的场景;gRPC 模式客户端由共享引用管理,不受此 CVE 影响; - 验证手段:可通过单元测试 test/extensions/filters/http/ext_authz/ext_authz_filter_test.cc 与集成测试 test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc 验证拒绝路径与流销毁的稳定性;
- 配置参考:HTTP 模式的配置示例可参考 test/extensions/filters/http/ext_authz/ext_authz.yaml,过滤器完整配置项定义位于 api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto。
结语
CVE-2026-50572 的修复看似只是一行 changelog,背后却是 Envoy 对"HTTP 模式下按流创建客户端 + 拒绝路径立即终止流"这一组合场景中对象生命周期严谨性的重新审视。从active_client_的解绑时机、状态机管理到流销毁取消协议,source/extensions/filters/http/ext_authz/ext_authz.cc 中的每一处细节都体现了 C++ 网络代理在异步生命周期管理上的典型工程实践。对于运行 HTTP 模式 ext_authz 的服务网格与 API 网关,理解这一修复不仅是安全合规的要求,更能帮助你在遇到类似"流终止与异步回调竞态"问题时快速定位根因。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考