news 2026/9/12 19:01:01

Envoy jwt_authn 过滤器安全加固:Filter-Wide Payload/Claim 头部清理机制深入解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Envoy jwt_authn 过滤器安全加固:Filter-Wide Payload/Claim 头部清理机制深入解析

Envoy jwt_authn 过滤器安全加固:Filter-Wide Payload/Claim 头部清理机制深入解析

【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy

导读

Envoy 的jwt_authn(JWT 认证)HTTP 过滤器在验证成功后,会通过forward_payload_headerclaim_to_headers将 JWT 的载荷与声明写入请求头,传递给上游服务。然而在旧版本中,这些头部的清理(sanitize)只发生在"匹配到的验证器内部",导致大量绕过路径存在身份伪造(spoofing)漏洞:客户端可以自行注入这些"保留头"并随请求直达上游。本文基于 Envoy 仓库中的变更记录(changelogs/current/minor_behavior_changes/jwt_authn__sanitize-payload-headers-filter-wide.rst),完整剖析这次 filter-wide(过滤器级)清理机制的变更内容、安全动机、源码实现、测试验证以及回滚开关,帮助你理解并安全升级。

背景:jwt_authn 如何把 JWT 身份"写入"请求

在 Envoy 中,jwt_authn过滤器(全名envoy.extensions.filters.http.jwt_authn)负责验证请求携带的 JWT,验证通过后可以把令牌中的可信信息透传给上游服务。它主要通过两种机制实现(定义见 api/envoy/extensions/filters/http/jwt_authn/v3/config.proto):

配置字段行为所在定义
forward_payload_header将验证成功的 JWT 载荷以base64url_encoded(jwt_payload_in_JSON)的格式写入指定头部,转发给后端;未指定则不转发config.proto#L256-L263
claim_to_headers将 JWT 中指定的 claim 复制到 HTTP 头部;string/int/double/bool 类型原样复制,array/object 类型序列化为 JSON 后 Base64 编码config.proto#L359-L372

其中claim_to_headers是可重复字段,每个条目由JwtClaimToHeader消息描述(config.proto#L907-L961),包含三个关键字段:

  • header_name:承载 claim 的 HTTP 头部名,该头部专为 JWT claim 保留,任何其他值都会被覆盖;
  • claim_name:要复制的 claim 名称,按.分割以支持嵌套(如nested.claim.key);
  • claim_path:以显式段列表指定路径,用于处理 claim 名本身含点号(如 URL 命名空间形式的 OIDC claim)的场景。claim_nameclaim_path必须且只能设置一个。

一个典型配置(摘自 docs/root/configuration/http/http_filters/_include/jwt-authn-claim-filter.yaml#L32-L41):

http_filters: - name: envoy.extensions.filters.http.jwt_authn typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication providers: provider_name2: issuer: https://example2.com claim_to_headers: - header_name: x-jwt-claim-sub claim_name: sub - header_name: x-jwt-claim-nested-key claim_name: nested.claim.key - header_name: x-jwt-tenants claim_name: tenants local_jwks: inline_string: "..." # 本地内联 JWKS

验证成功后,这些头部会携带如下格式的值发往上游(docs/root/configuration/http/http_filters/jwt_authn_filter.rst#L215-L221):

x-jwt-claim-sub: <JWT Claim> x-jwt-claim-nested-key: <JWT Claim> x-jwt-tenants: <Base64 encoded JSON JWT Claim>

安全前提:上游服务依赖这些头部时,默认它们"只可能是 Envoy 在验证通过后写入的可信值"。这一前提正是本次变更要保护的。

旧行为的安全缺陷:Sanitize 只发生在验证器内部

本次变更记录明确指出了旧实现的两类问题(变更记录原文):

Previously those headers were sanitized only inside the matched verifier, so paths that bypassed verification (emptyrequires, per-routedisabled, or CORS preflight bypass) could forward client-supplied values upstream, and a request authenticated by one provider could retain spoofed payload/claim headers configured on another provider.

归纳为两个具体漏洞场景:

场景一:绕过验证路径直接透传客户端伪造值

以下三种路径不需要任何 JWT 验证即可放行(返回Continue),但在旧实现中不会触发头部清理

  1. 空的requiresRequirementRule没有设置requirement_type时,匹配后直接放行;
  2. per-routedisabled:路由级配置通过PerRouteFilterConfig将过滤器标记为disabled(对应 filter_config.cc 中findPerRouteVerifier返回空验证器);
  3. CORS preflight 绕过bypass_cors_preflight启用且当前请求是 preflight 时直接放行(filter.cc#L64-L72)。

在这些路径上,客户端可以预先自行设置x-jwt-claim-subx-jwt-claim-nested-key等保留头,旧版本 Envoy 会原样转发给上游。而上游无法区分该值究竟来自 Envoy 的可信写入还是客户端伪造——身份伪装攻击(impersonation)就此成立

场景二:多 Provider 间的头部"串台"

当一个过滤器配置了多个 provider,且各自配置了不同的forward_payload_header/claim_to_headers时:请求被 provider A 认证通过后,A 的验证器只会清理/写入自己配置的头部;而 provider B 配置的那些头部,其客户端伪造值仍残留在请求中,随 provider A 的认证结果一起被放行上游,造成"一个提供方认证的请求却携带另一个提供方配置的伪造头部"的混乱状态。

新行为:Filter-Wide 头部清理

变更后的行为一句话概括(变更记录原文):

Thejwt_authnHTTP filter now strips every configuredforward_payload_headerandclaim_to_headersheader name from the requestbeforeapplying rules.

即:在应用任何匹配/验证规则之前,过滤器就把"所有 provider 配置的forward_payload_headerclaim_to_headers头部名"从请求中无条件剥离。这些头部从此真正成为"仅由 Envoy 在验证通过后写入的保留头",客户端在任何路径上都无法携带它们穿透过滤器。

源码级实现剖析

1. 一次构建:收集所有需要清理的头部名

在过滤器配置构造阶段(source/extensions/filters/http/jwt_authn/filter_config.cc#L65-L69),实现代码显式注释了设计意图:

// Union of every provider's forward_payload_header and claim_to_headers names. Built once so // Filter::decodeHeaders can sanitize before any verifier bypass path returns Continue. if (!all_providers.empty()) { header_sanitizer_ = Extractor::create(all_providers); }

这里基于全部 provider 的并集创建了一个Extractor实例(存于header_sanitizer_,声明见 filter_config.h#L161)。注意"并集"这个细节——它正是为了修复场景二的多 provider 串台问题:每个 provider 配置的保留头都会被纳入清理名单,而不仅是"当前匹配到的那个"。

ExtractorImpl::addProvider负责收集头部名(source/extensions/filters/http/jwt_authn/extractor.cc#L229-L235):

if (!provider.forward_payload_header().empty()) { headers_to_sanitize_.emplace_back(provider.forward_payload_header()); } for (const auto& header_and_claim : provider.claim_to_headers()) { headers_to_sanitize_.emplace_back(header_and_claim.header_name()); }

forward_payload_header非空即收集;claim_to_headers中每一个条目的header_name都收集,统一存入std::vector<LowerCaseString> headers_to_sanitize_(extractor.cc#L200),以大小写无关形式存储,确保 HTTP 头名匹配不区分大小写。

2. 早于一切分支:decodeHeaders 第一步即清理

关键调用点在过滤器入口 source/extensions/filters/http/jwt_authn/filter.cc#L52-L62:

Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool) { ... // Sanitize before any bypass decision when the reloadable feature is enabled (default). // Payload and claim headers are reserved for values this filter writes after verification; // leaving client-supplied values in place on no-verifier paths (empty requires, per-route // disabled, CORS preflight) would forward spoofed identity upstream. config_->sanitizePayloadHeaders(headers); ... }

这段代码的注释直接复述了本次变更的安全动机:payload 与 claim 头专为过滤器验证后写入的值保留;在无验证器路径上(空的requires、per-routedisabled、CORS preflight)如果保留客户端原值,就会把伪造身份转发到上游。因此清理必须先于所有绕过决策执行——这正是本次"filter-wide"语义的核心:无论后续走哪条分支,头部都已被处理。

3. 运行时开关与最终剥离动作

sanitizePayloadHeaders的实现(source/extensions/filters/http/jwt_authn/filter_config.h#L96-L105):

void sanitizePayloadHeaders(Http::RequestHeaderMap& headers) const override { // Behavior change vs pre-filter-wide sanitization: guard so operators can // disable during rollout if a deployment relied on client-supplied payload // / claim headers on bypass paths. if (header_sanitizer_ != nullptr && Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.jwt_authn_sanitize_payload_headers_filter_wide")) { header_sanitizer_->sanitizeHeaders(headers); } }

最终剥离动作非常简单直接(source/extensions/filters/http/jwt_authn/extractor.cc#L347-L351):

void ExtractorImpl::sanitizeHeaders(Http::RequestHeaderMap& headers) const { for (const auto& header : headers_to_sanitize_) { headers.remove(header); } }

遍历预先收集的头部名并逐一remove。这保证了:如果后续验证失败或走绕过路径,这些头在到达上游前已被删除;如果验证成功,则由验证器在认证后重新写入可信值(claim_to_headers的语义仍是"已有其他值则替换为 claim 值",见 jwt_authn_filter.rst#L202)。

测试验证:绕过路径必须清理

仓库中的单元测试直接覆盖了本次变更的两个关键绕过场景(test/extensions/filters/http/jwt_authn/filter_test.cc):

测试一:无匹配规则也要清理(filter_test.cc#L316-L327)

// Bypass paths must still sanitize payload/claim headers before Continue. TEST_F(FilterTest, TestNoRequirementMatchedSanitizesPayloadHeaders) { EXPECT_CALL(*mock_config_.get(), sanitizePayloadHeaders(_)) .WillOnce(Invoke([](Http::RequestHeaderMap& headers) { headers.remove(Http::LowerCaseString("x-jwt-claim-sub")); })); EXPECT_CALL(*mock_config_.get(), findVerifier(_, _)).WillOnce(Return(nullptr)); auto headers = Http::TestRequestHeaderMapImpl{{"x-jwt-claim-sub", "spoofed"}}; EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); EXPECT_FALSE(headers.has("x-jwt-claim-sub")); EXPECT_EQ(1U, mock_config_->stats().allowed_.value()); }

模拟请求携带伪造的x-jwt-claim-sub: spoofed,验证器匹配返回nullptr(即无 requirement 放行路径),断言最终Continue后该头部已被移除。

测试二:per-route disabled 绕过也要清理(filter_test.cc#L330-L347)

// Per-route disabled bypass must sanitize before Continue. TEST_F(FilterTest, TestPerRouteBypassSanitizesPayloadHeaders) { ... EXPECT_CALL(*mock_config_.get(), findPerRouteVerifier(_)) .WillOnce(Return(std::make_pair(nullptr, EMPTY_STRING))); EXPECT_CALL(*mock_config_.get(), sanitizePayloadHeaders(_)) .WillOnce(Invoke([](Http::RequestHeaderMap& headers) { headers.remove(Http::LowerCaseString("sec-istio-auth-userinfo")); })); auto headers = Http::TestRequestHeaderMapImpl{{"sec-istio-auth-userinfo", "spoofed"}}; EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); EXPECT_FALSE(headers.has("sec-istio-auth-userinfo")); EXPECT_EQ(1U, mock_config_->stats().allowed_.value()); }

findPerRouteVerifier返回空验证器(对应 per-routedisabled的路径,与 filter_config.cc#L126-L130 中per_route.config().disabled()的处理一致),断言sec-istio-auth-userinfo(一个常见的服务网格场景自定义头)的伪造值同样被清除。

两个测试的测试名注释都写着 "Bypass paths must still sanitize payload/claim headers before Continue",精确对应变更记录中列出的绕过路径。

运行时开关:如何控制与回滚

本次变更由 reloadable feature(可重载特性)控制,定义于 source/common/runtime/runtime_features.cc#L100:

RUNTIME_GUARD(envoy_reloadable_features_jwt_authn_sanitize_payload_headers_filter_wide);
属性
特性名envoy.reloadable_features.jwt_authn_sanitize_payload_headers_filter_wide
默认值true(变更默认开启)
关闭方式在 bootstrap 的runtime层中设置该 key 为false

默认开启意味着升级后行为立即收紧:所有配置了forward_payload_header/claim_to_headersjwt_authn过滤器,无论请求走何种路径,客户端注入的这些保留头都会被剥离。实现注释(filter_config.h#L97-L99)明确说明了开关的用途:如果某个部署此前在绕过路径上依赖客户端提供的 payload/claim 头(一种不安全但确实存在过的用法),运维方可以在滚动升级期间临时关闭该特性以保持行为一致,待业务侧适配后再开启。

从变更分类看,它属于"minor behavior change"而非 bug fix 或 breaking change——因为对绝大多数正确使用jwt_authn的部署而言,行为只会变得更安全;只有那些依赖不安全旧语义的边缘部署才可能受到影响,这正是提供回滚开关的原因。

升级建议与最佳实践

  1. 检查配置:梳理所有jwt_authn过滤器配置中的forward_payload_headerclaim_to_headers,确认上游服务对这些头的消费逻辑——它们必须被当作"仅可信来源"处理。
  2. 确认无绕过依赖:审查是否存在依赖empty requires、per-routedisabled或 CORS preflight 路径携带自定义x-jwt-*头的行为;若有,升级前需改造。
  3. 多 provider 场景:如果同一过滤器配置多个 provider 且各自定义了不同的保留头,升级后任一 provider 配置的保留头都会被全量剥离——这是修复后的预期行为,上游不应再收到任何 provider 的伪造值。
  4. 滚动升级:默认值即为安全值,无需额外开启;只有确认受影响时才通过 runtime 层临时false回滚,并尽快重新开启。

延伸阅读

  • 完整配置文档:docs/root/configuration/http/http_filters/jwt_authn_filter.rst(含 claim_to_headers 嵌套 claim、claim_path 用法及统计数据说明)
  • 完整可运行示例:docs/root/configuration/http/http_filters/_include/jwt-authn-claim-filter.yaml
  • Proto API 定义:api/envoy/extensions/filters/http/jwt_authn/v3/config.proto
  • 过滤器核心实现:source/extensions/filters/http/jwt_authn/filter.cc、source/extensions/filters/http/jwt_authn/filter_config.cc、source/extensions/filters/http/jwt_authn/extractor.cc
  • 单元测试:test/extensions/filters/http/jwt_authn/filter_test.cc
  • 变更记录原文:changelogs/current/minor_behavior_changes/jwt_authn__sanitize-payload-headers-filter-wide.rst

【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 19:00:32

Rubix 基础层:从云端到战车,同一套底座怎么跑

聊 Palantir 的架构&#xff0c;前面几篇我们把 Foundry、Ontology、AIP 都过了一遍。这些东西看起来各自独立&#xff0c;但有个问题我一直没正面回答&#xff1a;它们到底跑在哪&#xff1f; 一个客户在 AWS 上跑着 Foundry&#xff0c;另一头部队的单兵终端里跑着 AIP&#…

作者头像 李华
网站建设 2026/9/12 19:00:18

车载音频PAL架构与ResourceManager资源调度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 18:58:30

Java数据库与数据存储:Redis

1. 引言 在当今互联网高并发场景下&#xff0c;数据库的性能瓶颈往往成为系统扩展的最大障碍。Redis 作为一款高性能的内存数据库&#xff0c;凭借其丰富的数据结构、极快的读写速度和灵活的持久化机制&#xff0c;已经成为 Java 后端开发中不可或缺的组件。 本文将系统性地介绍…

作者头像 李华
网站建设 2026/9/12 18:58:23

网络摄像机首次播放音视频延时出图像踩坑记录

ZLMediaKit / RTSP / H.264 播放音视频踩坑记录 - SDP、SPS、PPS 完整详解适用场景&#xff1a;RTSP 信令交互、addStreamProxy 拉流、WebRTC、H264 裸流、花屏、首帧黑屏、无法解码&#xff0c;绝大多数问题根源就是 SDP / SPS/PPS。1、SDP 是什么 SDP&#xff1a;Session Des…

作者头像 李华
网站建设 2026/9/12 18:58:05

计算机硬件基础知识点:构建系统级故障推理框架

1. 这不是教科书&#xff0c;是十年装机维修老手的“硬件认知地图”你打开电脑前&#xff0c;有没有想过&#xff1a;为什么插上电源线&#xff0c;主板上的LED灯会亮&#xff1f;为什么换一根内存条&#xff0c;系统就蓝屏了&#xff1f;为什么硬盘灯狂闪时&#xff0c;鼠标却…

作者头像 李华
网站建设 2026/9/12 18:57:53

ISP---RAW数据解析:从PixelFormat、Packed_Buffer到Bayer解码

相机SDK仅向应用层交付void* buffer裸字节流、图像宽高及基础元数据&#xff0c;如何将无结构的字节序列&#xff0c;还原为精准、无失真的二维RAW图像&#xff1f; RAW硬件-软件传输链路遵循固定范式&#xff1a; Sensor → PixelFormat → Transport Buffer → Unpack → 2D …

作者头像 李华