一个 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),受影响范围和修复版本如下:
| 系列 | 受影响 | 已修复 |
|---|---|---|
| 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(开源版) |
解析到其系列中达到或超过修复版本的 Spring Security 版本(或处于 7.0 / 6.5 之后的任何未来系列)的项目被视为不受影响,不会收到任何按接收点或按文件的标记。无法解析版本的项目则回退到常规的基于模式的检测,以便扫描器倾向于报告其无法证伪的发现。SpringSecurityVersionByProject 数据表仍会记录解析出的版本,并将每个项目标记为受影响或不受影响,以便您审计被过滤的内容。
以下代码看起来危险,但已被包装器跟踪,因此安全头会在响应提交之前写入:
| 代码 | 为何安全 |
|---|---|
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) 路由。 |
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.FindHttpResponseContentLengthHeader | 针对 "Content-Length" 字面量到达 setHeader / setIntHeader / addIntHeader(servlet)或 HttpHeaders.set / add(WebFlux)的污点流分析 |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | WebFlux 无条件提交:writeWith、writeAndFlushWith、setComplete、HttpHeaders.setContentLength |
运行以下配方:
| 配方 | 用途 |
|---|---|
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 断言安全头是缺失的,因此有效的修复会使其失败:
| 端点 | 修复前 | 修复后 |
|---|---|---|
/vuln/stream | X-Content-Type-Options: null | nosniff |
/vuln/content-length | X-Content-Type-Options: null | nosniff |
/safe | nosniff | nosniff |
X-Frame-Options 和 Cache-Control 遵循相同的模式。
在语料库中能够构建的 16 个仓库(共识别出 29 个)中,修复为三个生成了配置,并正确地将其余仓库保持原样:
| 仓库 | 结果 |
|---|---|
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 版本 |
在三个已修补的仓库上重新运行修复不会生成任何额外内容,因此该修复方案在真实项目上对其自身输出是幂等的。
第二个更广泛的语料库针对修复实际要解决的群体——任何受影响的 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 版本,但修复方案会被扣留,而不是发出无法编译的调用。