
OpenRewrite recipe that detects and fixes Spring Security header suppression (CVE-2026-22732) by identifying Content-Length header misuse and generating eager header-writing configuration.
OpenRewrite recipe that detects code susceptible to CVE-2026-22732, a Spring Security defect where setting Content-Length through one of three response methods bypasses Spring Security's OnCommittedResponseWrapper. Because the wrapper never sees the header, onResponseCommitted() never fires, and the lazy-added security headers (X-Frame-Options, X-Content-Type-Options, Cache-Control, etc.) are silently dropped.
The actual triggers, confirmed against vulnerable Spring Security 6.4.12 with Spring Boot 3.4.3 / embedded Tomcat:
Servlet Content-Length via the wrapper-bypassing overloads
response.setHeader("Content-Length", "42");
response.setIntHeader("Content-Length", 42);
response.addIntHeader("Content-Length", 42);
These three overloads are not overridden in OnCommittedResponseWrapper. Subsequent body writes complete the declared length and the container commits without firing the lazy header writer.
WebFlux Content-Length via HttpHeaders
serverHttpResponse.getHeaders().set("Content-Length", "42");
serverHttpResponse.getHeaders().add("Content-Length", "42");
serverHttpResponse.getHeaders().setContentLength(42L);
WebFlux unconditional response commits
serverHttpResponse.writeWith(Mono.just(dataBuffer));
serverHttpResponse.writeAndFlushWith(publisher);
serverHttpResponse.setComplete();
The recipe is gated on Spring Security presence — it emits nothing in files that don't reference any org.springframework.security.* type — and on affected Spring Security version ranges. Per the Spring advisory published 2026-03-19, the affected ranges and fix versions are:
| Series | Affected | Fixed |
|---|---|---|
| 5.7.x | 5.7.0 – 5.7.21 | 5.7.22 (Enterprise) |
| 5.8.x | 5.8.0 – 5.8.23 | 5.8.24 (Enterprise) |
| 6.3.x | 6.3.0 – 6.3.14 | 6.3.15 (Enterprise) |
| 6.4.x | 6.4.0 – 6.4.14 | 6.4.15 (Enterprise) |
| 6.5.x | 6.5.0 – 6.5.8 | 6.5.9 (OSS) |
| 7.0.x | 7.0.0 – 7.0.3 | 7.0.4 (OSS) |
Projects resolving a Spring Security version at or above the fix in its series (or on any future series past 7.0 / 6.5) are treated as unaffected and receive no per-sink or per-file markers. Projects where the version can't be resolved fall through to the usual pattern-based detection so a scanner errs toward reporting a finding it can't disprove. The SpringSecurityVersionByProject data table still records the resolved version and flags each project as affected or not, so you can audit what was filtered.
These look dangerous but are wrapper-tracked, so security headers are written before the response commits:
| Code | Why it's safe |
|---|---|
response.setContentLength(int) / setContentLengthLong(long) | Overridden — wrapper records the declared length and fires onResponseCommitted() when the body completes. |
response.flushBuffer() | Overridden — calls doOnResponseCommitted() before super.flushBuffer(). |
response.getOutputStream().write(..) / flush() / close() | Returns SaveContextServletOutputStream; every write/flush/close fires doOnResponseCommitted() before delegating. |
response.getWriter().write(..) / print(..) / println(..) / flush() / close() | Returns SaveContextPrintWriter; same pattern. |
response.addHeader("Content-Length", v) | Special-cased in the wrapper — routed through setContentLength(long). |
The Semgrep demo's /vuln/flush endpoint claims flushBuffer() is the trigger, but on a vulnerable Spring Security 6.4.12 the response actually returns all six security headers. The real triggers in the demo are the setIntHeader("Content-Length", ...) calls in /vuln/stream and /vuln/content-length.
Run this one:
| Recipe | Purpose |
|---|---|
io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression | Runs every detection and emits the version report table |
The aggregator above is composed of two smaller recipes. You can invoke them individually if you want only one detection.
| Recipe | Purpose |
|---|---|
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthHeader | Taint-flow for "Content-Length" literal reaching setHeader / setIntHeader / addIntHeader (servlet) or HttpHeaders.set / add (WebFlux) |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | WebFlux unconditional commits: writeWith, writeAndFlushWith, setComplete, HttpHeaders.setContentLength |
Run this one:
| Recipe | Purpose |
|---|---|
io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression | Picks the cheapest remediation each project can actually take |
It runs two steps in order.
1. Bump to the fix on the project's own series. Spring Security published the fix as 6.5.9
and 7.0.4 on Maven Central. Each bump is gated on a FindAffectedSpringSecuritySeries
precondition, because UpgradeDependencyVersion only checks that its target is newer — told to
go to 7.0.4 it would happily drag a 5.8 project across two major versions.
2. Add an eager header-writing configuration to whatever step 1 could not fix. This generates
one @Configuration class per project:
@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;
}
};
}
Writing the headers up front makes it irrelevant whether the wrapper ever observes the commit, so
this closes every sink in the project at once — including the ones taint analysis can't reach,
like the Map flow under "Known limitations". The BeanPostProcessor sees filters built by the
HttpSecurity DSL because AutowireBeanFactoryObjectPostProcessor initialises them through the
bean factory. Two independently published fixes for this CVE use exactly this shape
(hmcts/idam-web-public, and
armory-io's Spinnaker forks via the equivalent ObjectPostProcessor).
Step 2 is what covers the projects step 1 cannot help:
| Situation | Why the bump doesn't work |
|---|---|
| 5.7, 5.8, 6.3, 6.4 | Fix ships to Spring Enterprise subscribers only — not on Maven Central |
| 6.0 - 6.2 | No fix was ever released on those series |
| Version managed by an imported BOM | Nothing is declared locally for the bump to edit |
That last row is not a corner case. nla/bamboo resolves 7.0.3 — a series with an open-source
fix — entirely from the Spring Boot BOM, so a version check alone would skip it under both steps
and leave it vulnerable. AddEagerHeaderWriterConfiguration therefore only defers to the upgrade
when the project both has an open-source fix available and declares a version of its own.
The generated class is placed next to an @EnableWebSecurity class where one exists, falling back
to @SpringBootApplication and then any @Configuration, so it always lands somewhere component
scanning reaches. Projects that already write headers eagerly, or that vendor a patched
OnCommittedResponseWrapper (as jogetworkflow/jw-community
does), are left alone.