一个 OpenRewrite 配方,用于检测易受 CVE-2026-22732 影响的代码。这是一个 Spring Security 缺陷,通过三种响应方法之一设置 Content-Length 会绕过 Spring Security 的 OnCommittedResponseWrapper。由于包装器从未看到该头,onResponseCommitted() 永远不会触发,延迟添加的安全头(X-Frame-Options、X-Content-Type-Options、Cache-Control 等)会被静默丢弃。
实际触发点,已在易受攻击的 Spring Security 6.4.12(搭配 Spring Boot 3.4.3 / 内嵌 Tomcat)上验证:
通过绕过包装器的重载方法设置 Servlet Content-Length
response.setHeader("Content-Length", "42");
response.setIntHeader("Content-Length", 42);
response.addIntHeader("Content-Length", 42);
这三个重载方法在 OnCommittedResponseWrapper 中没有被覆盖。后续的 body 写入完成了声明的长度,容器在未触发延迟安全头写入器的情况下提交了响应。
通过 HttpHeaders 设置 WebFlux Content-Length
serverHttpResponse.getHeaders().set("Content-Length", "42");
serverHttpResponse.getHeaders().add("Content-Length", "42");
serverHttpResponse.getHeaders().setContentLength(42L);
WebFlux 无条件响应提交
serverHttpResponse.writeWith(Mono.just(dataBuffer));
serverHttpResponse.writeAndFlushWith(publisher);
serverHttpResponse.setComplete();
该配方以 Spring Security 的存在为前提——在未引用任何 org.springframework.security.* 类型的文件中不会产生任何输出——并且以受影响的 Spring Security 版本范围为前提。根据 Spring 公告(发布于 2026-03-19),受影响范围和修复版本如下:
解析到其系列中达到或超过修复版本的 Spring Security 版本(或处于 7.0 / 6.5 之后的任何未来系列)的项目被视为不受影响,不会收到任何按接收点或按文件的标记。无法解析版本的项目则回退到常规的基于模式的检测,以便扫描器倾向于报告其无法证伪的发现。SpringSecurityVersionByProject 数据表仍会记录解析出的版本,并将每个项目标记为受影响或不受影响,以便您审计被过滤的内容。
以下代码看起来危险,但已被包装器跟踪,因此安全头会在响应提交之前写入:
Semgrep 演示的 /vuln/flush 端点声称 flushBuffer() 是触发点,但在易受攻击的 Spring Security 6.4.12 上,响应实际上返回了全部六个安全头。演示中真正的触发点是 /vuln/stream 和 /vuln/content-length 中的 setIntHeader("Content-Length", ...) 调用。
运行以下配方:
| 配方 | 用途 |
|---|---|
io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression | 运行所有检测并输出版本报告表 |
上述聚合配方由两个较小的配方组成。如果您只需要其中一种检测,可以单独调用它们。
运行以下配方:
| 配方 | 用途 |
|---|---|
io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression | 为每个项目选择其实际可采取的最经济修复方案 |
它按顺序执行两个步骤。
1. 升级到项目自身系列上的修复版本。 Spring Security 已在 Maven Central 上发布了 6.5.9 和 7.0.4 修复版本。每次升级都以 FindAffectedSpringSecuritySeries 前置条件为门槛,因为 UpgradeDependencyVersion 只检查其目标是否更新——如果告诉它升级到 7.0.4,它会乐意将 5.8 项目跨越两个主版本拖上去。
2. 为步骤 1 无法修复的任何内容添加主动写入安全头的配置。 这会为每个项目生成一个 @Configuration 类:
@Bean
public static BeanPostProcessor eagerHeaderWriterFilterBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof HeaderWriterFilter) {
((HeaderWriterFilter) bean).setShouldWriteHeadersEagerly(true);
}
return bean;
}
};
}
提前写入安全头使得包装器是否观察到提交变得无关紧要,因此这会一次性关闭项目中的所有接收点——包括污点分析无法触及的接收点,例如“已知限制”下的 Map 流。BeanPostProcessor 能看到 HttpSecurity DSL 构建的过滤器,因为 AutowireBeanFactoryObjectPostProcessor 通过 bean 工厂初始化它们。针对此 CVE 的两个独立发布的修复方案恰好使用了这种形式(hmcts/idam-web-public,以及 armory-io 的 Spinnaker 分支通过等效的 ObjectPostProcessor)。
步骤 2 覆盖了步骤 1 无法帮助的项目:
| 情况 | 为何升级不起作用 |
|---|---|
| 5.7、5.8、6.3、6.4 | 修复仅提供给 Spring Enterprise 订阅用户——不在 Maven Central 上 |
| 6.0 - 6.2 | 这些系列从未发布过修复 |
| 版本由导入的 BOM 管理 | 本地没有声明任何内容可供升级编辑 |
最后一行并非边缘情况。nla/bamboo 完全从 Spring Boot BOM 解析出 7.0.3——一个有开源修复的系列——因此仅版本检查会在两个步骤下都跳过它,使其保持易受攻击状态。因此,AddEagerHeaderWriterConfiguration 仅在项目同时拥有可用的开源修复且声明了自己的版本时才让位于升级。
生成的类被放置在存在 @EnableWebSecurity 类的旁边,回退到 @SpringBootApplication,然后是任何 @Configuration,因此它总能落在组件扫描可达的位置。已经主动写入安全头的项目,或内置了打过补丁的 OnCommittedResponseWrapper 的项目(如 jogetworkflow/jw-community 所做),则不会被改动。
| 配方 | 用途 |
|---|---|
io.moderne.recipe.cve202622732.UpgradeSpringSecurityToPatchedVersion | 按系列限定的升级到 6.5.9 / 7.0.4 |
io.moderne.recipe.cve202622732.AddEagerHeaderWriterConfiguration | 生成的配置,单独使用 |
io.moderne.recipe.cve202622732.FindAffectedSpringSecuritySeries | 标记处于受影响系列上的项目;升级前置条件 |
已应用于 semgrep/cve-2026-22732-demo,运行在易受攻击的 Spring Security 6.4.12 上,针对内嵌 Tomcat。其 HeaderVerificationTest 断言安全头是缺失的,因此有效的修复会使其失败:
X-Frame-Options 和 Cache-Control 遵循相同的模式。
在语料库中能够构建的 16 个仓库(共识别出 29 个)中,修复为三个生成了配置,并正确地将其余仓库保持原样:
在三个已修补的仓库上重新运行修复不会生成任何额外内容,因此该修复方案在真实项目上对其自身输出是幂等的。
第二个更广泛的语料库针对修复实际要解决的群体——任何受影响的 servlet Spring Security 应用程序,因为不需要接收点。通过代码搜索找到的 64 个此类项目中,52 个可构建,48 个解析到受影响版本,38 个已修补;其中 35 个可编译(另外三个在未生成文件的情况下同样失败)。所有 8 个 Spring Security 版本低于 5.2 的项目都被正确跳过。参见 SUSCEPTIBLE-REPOSITORIES.md 第 8 节。
OnCommittedResponseWrapper extends jakarta.servlet.http.HttpServletResponseWrapper,因此 CVE-2026-22732 仅影响 servlet,响应式应用程序不会暴露于该漏洞。FindHttpResponseContentLengthOrFlushBuffer 的发现标记了类似的响应式模式,仍需要人工审查,但修复方案有意不对其采取行动。AddEagerHeaderWriterConfiguration 会跳过任何能看到 HeaderWriterFilter 但背后没有 servlet API 的模块——该过滤器继承自 OncePerRequestFilter,而 spring-security-web 将 servlet API 作为非传递的 provided 依赖携带,因此在那里生成会以 cannot access jakarta.servlet.Filter 失败(在 apache/shenyu 上观察到)。HeaderWriterFilter.setShouldWriteHeadersEagerly 在 5.2 中引入;在 4.0.4 上,该过滤器只有构造函数和 doFilterInternal。检测仍会报告 EOL 版本,但修复方案会被扣留,而不是发出无法编译的调用。| 表 | 行 |
|---|---|
TaintFlowTable(来自 rewrite-program-analysis) | 每个 Content-Length 头污点命中一行 |
HttpResponseDirectCommitTable | 每个 WebFlux 结构性命中一行 |
SpringSecurityVersionByProject | 每个检测到 Spring Security 版本的项目一行 |
通过 Moderne CLI:
mod run . --recipe io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression
通过 rewrite.yml:
---
type: specs.openrewrite.org/v1beta/recipe
name: com.example.DetectSpringSecurityHeaderSuppression
displayName: Detect CVE-2026-22732
recipeList:
- io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression
repos.csv 列出了这些配方开发和测量所依据的 75 个公共仓库,并固定到每个仓库被评估时的确切提交。其中几个仓库正在积极维护,并将在上游修补,因此 changeset 列是使以下数字可复现而非仅仅看似合理的关键。
mod git sync csv ./corpus repos.csv --with-sources
mod build ./corpus
mod run ./corpus --recipe=io.moderne.recipe.cve202622732.CveDevCenter
mod devcenter ./corpus --last-recipe-run
同步大约需要 20 秒和 1.4 GB;构建大约需要 15 分钟,是唯一较慢的步骤。mod devcenter 将 devcenter.html 写入 corpus/.moderne/run/<runId>/,每个组织子目录各一份。
org1 列将语料库分为反映每个仓库存在原因的组——Servlet Sinks 和 WebFlux Sinks 对应两种易受攻击的调用形态,Patched 对应已在上游修复的仓库,Reference 对应 Spring Security 本身和其他非消费者,Verified 对应已针对运行中的服务器验证的情况,Gradle 对应构建工具覆盖,Wide 对应批量样本。
在固定的提交上,预期:
| 结果 | |
|---|---|
| 升级卡片 | 39 个 Major、21 个 Minor、6 个 Patch、4 个 Completed(70 个仓库) |
| 安全卡片 | 65 个暴露的仓库 |
| 不适用 | 5 个仓库未解析到任何 Spring Security 依赖 |
其中五个中的四个确实不使用 Spring Security——spring-projects/spring-security 是库本身,JoeyBling/bootplus 使用 Apache Shiro,jenkinsci/stapler 直接针对 servlet API,infofabrik/reportserver 没有 Maven 或 Gradle 构建可解析。第五个 xtuer/template-app 声明了 spring-security-web:5.0.0.RELEASE,但其 Gradle 构建在 mod build 期间根本没有解析任何依赖,因此没有配方能看到该版本。将其视为未测量而非不受影响。
要应用修复并检查结果:
mod run ./corpus --recipe=io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression
mod git apply ./corpus --last-recipe-run
mod exec ./corpus --last-recipe-run MODERNE_BUILD_TOOL_CHECK
mod git apply 就地写入检出目录。对整个语料库进行完整的 check 很慢,并且会暴露与此更改无关的失败——缺少 JDK 工具链、无法访问的依赖仓库、本来就失败的测试——因此有意义的信号是与应用前运行相同命令的差异。
来自 rewrite-program-analysis 的污点分析处理本地数据流和按方法摘要,因此以下模式会被自动检测:
String h = "Content-Length"; response.setIntHeader(h, 42); 会被标记——框架通过本地赋值跟踪字面量污点。Map、List、自定义集合)的流。 stash.put("k", "Content-Length") 后跟 response.setIntHeader(stash.get("k"), 42) 不会被检测——put/get 的同一性对分析来说是不透明的。Moderne 专有。仅限 Moderne 客户根据商业合同条款使用。
| 系列 | 受影响 | 已修复 |
|---|
| 5.7.x | 5.7.0 – 5.7.21 | 5.7.22(企业版) |
| 5.8.x | 5.8.0 – 5.8.23 | 5.8.24(企业版) |
| 6.3.x | 6.3.0 – 6.3.14 | 6.3.15(企业版) |
| 6.4.x | 6.4.0 – 6.4.14 | 6.4.15(企业版) |
| 6.5.x | 6.5.0 – 6.5.8 | 6.5.9(开源版) |
| 7.0.x | 7.0.0 – 7.0.3 | 7.0.4(开源版) |
| 代码 | 为何安全 |
|---|
response.setContentLength(int) / setContentLengthLong(long) | 已被覆盖——包装器记录声明的长度,并在 body 完成时触发 onResponseCommitted()。 |
response.flushBuffer() | 已被覆盖——在 super.flushBuffer() 之前调用 doOnResponseCommitted()。 |
response.getOutputStream().write(..) / flush() / close() | 返回 SaveContextServletOutputStream;每次 write/flush/close 都会在委托之前触发 doOnResponseCommitted()。 |
response.getWriter().write(..) / print(..) / println(..) / flush() / close() | 返回 SaveContextPrintWriter;模式相同。 |
response.addHeader("Content-Length", v) | 在包装器中特殊处理——通过 setContentLength(long) 路由。 |
| 配方 | 用途 |
|---|
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthHeader | 针对 "Content-Length" 字面量到达 setHeader / setIntHeader / addIntHeader(servlet)或 HttpHeaders.set / add(WebFlux)的污点流分析 |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | WebFlux 无条件提交:writeWith、writeAndFlushWith、setComplete、HttpHeaders.setContentLength |
| 端点 | 修复前 | 修复后 |
|---|
/vuln/stream | X-Content-Type-Options: null | nosniff |
/vuln/content-length | X-Content-Type-Options: null | nosniff |
/safe | nosniff | nosniff |
| 仓库 | 结果 |
|---|
semgrep/cve-2026-22732-demo | 生成到 com/example/vuln,位于 @EnableWebSecurity 旁边;可编译,安全头已恢复 |
nla/bamboo | 生成到 ui/src/bamboo,位于 @SpringBootApplication 旁边;可编译。这是升级无法触及的 BOM 管理的 7.0.3 情况 |
star-whale/starwhale | 生成到 ai/starwhale/mlops/configuration/security,位于 @EnableWebSecurity 旁边;可编译(JDK 11,其声明的目标) |
hmcts/idam-web-public | 保持原样——已调用 setShouldWriteHeadersEagerly |
okta/okta-idx-java(6.5.9)、psi-probe(6.5.11)、Ant-Media-Server(6.5.11) | 保持原样——已超过其系列上的修复版本 |
apache/shenyu(6.3.1) | 保持原样——仅响应式;HeaderWriterFilter 背后没有 servlet API,且该 CVE 仅影响 servlet |
brutusin/Brutusin-RPC(4.0.4) | 保持原样——早于 setShouldWriteHeadersEagerly(5.2) |
bootplus、template-app、front50、igor、rosco、spring-security、reportserver | 保持原样——未解析到受影响的 Spring Security 版本 |