
Receta de OpenRewrite que detecta y corrige la supresión de cabeceras de Spring Security (CVE-2026-22732) identificando el uso incorrecto de la cabecera Content-Length y generando una configuración de escritura anticipada de cabeceras.
Receta de OpenRewrite que detecta código susceptible a CVE-2026-22732, un defecto de Spring Security donde establecer Content-Length a través de uno de tres métodos de respuesta omite el OnCommittedResponseWrapper de Spring Security. Debido a que el wrapper nunca ve la cabecera, onResponseCommitted() nunca se dispara, y las cabeceras de seguridad añadidas de forma diferida (X-Frame-Options, X-Content-Type-Options, Cache-Control, etc.) se descartan silenciosamente.
Los desencadenantes reales, confirmados contra Spring Security 6.4.12 vulnerable con Spring Boot 3.4.3 / Tomcat embebido:
Content-Length de Servlet mediante las sobrecargas que omiten el wrapper
response.setHeader("Content-Length", "42");
response.setIntHeader("Content-Length", 42);
response.addIntHeader("Content-Length", 42);
Estas tres sobrecargas no están sobrescritas en OnCommittedResponseWrapper. Las escrituras posteriores del cuerpo completan la longitud declarada y el contenedor confirma sin disparar el escritor de cabeceras diferido.
Content-Length de WebFlux mediante HttpHeaders
serverHttpResponse.getHeaders().set("Content-Length", "42");
serverHttpResponse.getHeaders().add("Content-Length", "42");
serverHttpResponse.getHeaders().setContentLength(42L);
Confirmaciones de respuesta incondicionales de WebFlux
serverHttpResponse.writeWith(Mono.just(dataBuffer));
serverHttpResponse.writeAndFlushWith(publisher);
serverHttpResponse.setComplete();
La receta está condicionada a la presencia de Spring Security — no emite nada en archivos que no referencian ningún tipo org.springframework.security.* — y a los rangos de versiones de Spring Security afectados. Según el aviso de Spring publicado el 2026-03-19, los rangos afectados y las versiones corregidas son:
| Serie | Afectada | Corregida |
|---|---|---|
| 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) |
Los proyectos que resuelven una versión de Spring Security igual o superior a la corrección en su serie (o en cualquier serie futura más allá de 7.0 / 6.5) se tratan como no afectados y no reciben marcadores por sumidero ni por archivo. Los proyectos donde la versión no se puede resolver recurren a la detección habitual basada en patrones, de modo que un escáner tiende a informar de un hallazgo que no puede refutar. La tabla de datos SpringSecurityVersionByProject sigue registrando la versión resuelta y marca cada proyecto como afectado o no, para que puedas auditar lo que se filtró.
Estos parecen peligrosos pero están rastreados por el wrapper, por lo que las cabeceras de seguridad se escriben antes de que la respuesta confirme:
| Código | Por qué es seguro |
|---|---|
response.setContentLength(int) / setContentLengthLong(long) | Sobrescrito — el wrapper registra la longitud declarada y dispara onResponseCommitted() cuando el cuerpo se completa. |
response.flushBuffer() | Sobrescrito — llama a doOnResponseCommitted() antes de super.flushBuffer(). |
response.getOutputStream().write(..) / flush() / close() | Devuelve SaveContextServletOutputStream; cada write/flush/close dispara doOnResponseCommitted() antes de delegar. |
response.getWriter().write(..) / print(..) / println(..) / flush() / close() | Devuelve SaveContextPrintWriter; mismo patrón. |
response.addHeader("Content-Length", v) | Caso especial en el wrapper — se enruta a través de setContentLength(long). |
El endpoint /vuln/flush de la demo de Semgrep afirma que flushBuffer() es el desencadenante, pero en un Spring Security 6.4.12 vulnerable la respuesta realmente devuelve las seis cabeceras de seguridad. Los desencadenantes reales en la demo son las llamadas setIntHeader("Content-Length", ...) en /vuln/stream y /vuln/content-length.
Ejecuta esta:
| Receta | Propósito |
|---|---|
io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression | Ejecuta todas las detecciones y emite la tabla de informe de versiones |
El agregador anterior se compone de dos recetas más pequeñas. Puedes invocarlas individualmente si solo quieres una detección.
| Receta | Propósito |
|---|---|
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthHeader | Flujo de contaminación para el literal "Content-Length" que llega a setHeader / setIntHeader / addIntHeader (servlet) o HttpHeaders.set / add (WebFlux) |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | Confirmaciones incondicionales de WebFlux: writeWith, writeAndFlushWith, setComplete, HttpHeaders.setContentLength |
Ejecuta esta:
| Receta | Propósito |
|---|---|
io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression | Elige la remediación más económica que cada proyecto puede aplicar realmente |
Ejecuta dos pasos en orden.
1. Actualizar a la corrección en la propia serie del proyecto. Spring Security publicó la corrección como 6.5.9
y 7.0.4 en Maven Central. Cada actualización está condicionada a una
precondición FindAffectedSpringSecuritySeries,
porque UpgradeDependencyVersion solo comprueba que su objetivo es más nuevo — si se le indica
ir a 7.0.4 arrastraría felizmente un proyecto 5.8 a través de dos versiones principales.
2. Añadir una configuración de escritura de cabeceras anticipada a lo que el paso 1 no pudo corregir. Esto genera
una clase @Configuration por proyecto:
@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;
}
};
}
Escribir las cabeceras por adelantado hace irrelevante si el wrapper observa alguna vez la confirmación, por lo que
esto cierra todos los sumideros del proyecto a la vez — incluidos aquellos a los que el análisis de contaminación no puede llegar,
como el flujo Map en "Limitaciones conocidas". El BeanPostProcessor ve los filtros construidos por el
DSL HttpSecurity porque AutowireBeanFactoryObjectPostProcessor los inicializa a través de la
fábrica de beans. Dos correcciones publicadas de forma independiente para este CVE usan exactamente esta forma
(hmcts/idam-web-public, y
los forks de Spinnaker de armory-io mediante el ObjectPostProcessor equivalente).
El paso 2 es lo que cubre los proyectos a los que el paso 1 no puede ayudar:
| Situación | Por qué la actualización no funciona |
|---|---|
| 5.7, 5.8, 6.3, 6.4 | La corrección se envía solo a suscriptores de Spring Enterprise — no está en Maven Central |
| 6.0 - 6.2 | Nunca se publicó una corrección en esas series |
| Versión gestionada por un BOM importado | No se declara nada localmente para que la actualización lo edite |
Esa última fila no es un caso límite. nla/bamboo resuelve 7.0.3 — una serie con una corrección
de código abierto — enteramente desde el BOM de Spring Boot, por lo que una comprobación de versión sola lo omitiría en ambos pasos
y lo dejaría vulnerable. AddEagerHeaderWriterConfiguration por tanto solo difiere a la actualización
cuando el proyecto tiene una corrección de código abierto disponible y declara una versión propia.
La clase generada se coloca junto a una clase @EnableWebSecurity donde existe, recurriendo
a @SpringBootApplication y luego a cualquier @Configuration, de modo que siempre aterriza en algún lugar al que
el escaneo de componentes llega. Los proyectos que ya escriben cabeceras de forma anticipada, o que incluyen un
OnCommittedResponseWrapper parcheado (como hace jogetworkflow/jw-community),
se dejan intactos.
| Receta | Propósito |
|---|---|
io.moderne.recipe.cve202622732.UpgradeSpringSecurityToPatchedVersion | Actualización condicionada por serie a 6.5.9 / 7.0.4 |
io.moderne.recipe.cve202622732.AddEagerHeaderWriterConfiguration | La configuración generada, por sí sola |
io.moderne.recipe.cve202622732.FindAffectedSpringSecuritySeries | Marca proyectos en una serie afectada; la precondición de la actualización |
Aplicado a semgrep/cve-2026-22732-demo en
Spring Security 6.4.12 vulnerable, contra Tomcat embebido. Su HeaderVerificationTest afirma
que las cabeceras de seguridad están ausentes, por lo que una corrección que funcione hace que falle:
| Endpoint | Antes | Después |
|---|---|---|
/vuln/stream | X-Content-Type-Options: null | nosniff |
/vuln/content-length | X-Content-Type-Options: null | nosniff |
/safe | nosniff | nosniff |
X-Frame-Options y Cache-Control siguen el mismo patrón.
En los 16 repositorios del corpus que compilan (de 29 identificados), la corrección generó una configuración para tres y dejó correctamente intactos al resto:
| Repositorio | Resultado |
|---|---|
semgrep/cve-2026-22732-demo | Generado en com/example/vuln, junto a @EnableWebSecurity; compila, cabeceras restauradas |
nla/bamboo | Generado en ui/src/bamboo, junto a @SpringBootApplication; compila. El caso 7.0.3 gestionado por BOM al que la actualización no puede llegar |
star-whale/starwhale | Generado en ai/starwhale/mlops/configuration/security, junto a @EnableWebSecurity; compila (JDK 11, su objetivo declarado) |
hmcts/idam-web-public | Dejado intacto — ya llama a setShouldWriteHeadersEagerly |
okta/okta-idx-java (6.5.9), psi-probe (6.5.11), Ant-Media-Server (6.5.11) | Dejados intactos — más allá de la corrección en su serie |
apache/shenyu (6.3.1) | Dejado intacto — solo reactivo; HeaderWriterFilter no tiene API de servlet detrás, y el CVE es solo de servlet |
brutusin/Brutusin-RPC (4.0.4) | Dejado intacto — anterior a setShouldWriteHeadersEagerly (5.2) |
bootplus, template-app, front50, igor, rosco, spring-security, reportserver | Dejados intactos — no se resolvió ninguna versión de Spring Security afectada |
Re-ejecutar la corrección sobre los tres repositorios parcheados no genera nada más, por lo que la remediación es idempotente contra su propia salida en proyectos reales.
Un segundo corpus más amplio apunta a la población que la corrección realmente aborda — cualquier aplicación servlet de Spring Security afectada, ya que no se requiere ningún sumidero. De los 64 proyectos de este tipo encontrados mediante búsqueda de código, 52 compilaron, 48 resolvieron una versión afectada, y 38 fueron parcheados; 35 de esos compilan (los otros tres fallan de forma idéntica sin el archivo generado). Los 8 proyectos en Spring Security inferior a 5.2 se omitieron correctamente. Consulta la sección 8 de SUSCEPTIBLE-REPOSITORIES.md.
OnCommittedResponseWrapper extends jakarta.servlet.http.HttpServletResponseWrapper, por lo que
CVE-2026-22732 es solo de servlet y una aplicación reactiva no está expuesta a él. Los hallazgos de
FindHttpResponseContentLengthOrFlushBuffer marcan el patrón reactivo análogo y aún necesitan
revisión manual, pero la corrección deliberadamente no actúa sobre ellos. AddEagerHeaderWriterConfiguration
omite cualquier módulo que pueda ver HeaderWriterFilter sin la API de servlet detrás — el
filtro extiende OncePerRequestFilter, y spring-security-web lleva la API de servlet como una
dependencia provided no transitiva, por lo que generar allí falla con
cannot access jakarta.servlet.Filter (observado en apache/shenyu).HeaderWriterFilter.setShouldWriteHeadersEagerly llega en 5.2; en 4.0.4 el filtro solo tiene un
constructor y doFilterInternal. La detección sigue informando de versiones EOL, pero la remediación se
retiene en lugar de emitir una llamada que no puede compilar.| Tabla | Filas |
|---|---|
TaintFlowTable (de rewrite-program-analysis) | Una fila por acierto de contaminación de cabecera Content-Length |
HttpResponseDirectCommitTable | Una fila por acierto estructural de WebFlux |
SpringSecurityVersionByProject | Una fila por proyecto con versión de Spring Security detectada |
Mediante la CLI de Moderne:
mod run . --recipe io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression
Mediante 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 lista los 75 repositorios públicos contra los que estas recetas se desarrollaron y midieron,
fijados al commit exacto en el que cada uno se evaluó. Varios se mantienen activamente
y se parchearán upstream, por lo que la columna changeset es lo que hace que los números siguientes
sean reproducibles en lugar de meramente plausibles.
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
La sincronización tarda unos 20 segundos y 1,4 GB; la compilación tarda aproximadamente 15 minutos y es el
único paso lento. mod devcenter escribe devcenter.html en
corpus/.moderne/run/<runId>/, más uno por subdirectorio de organización.
La columna org1 divide el corpus en grupos que reflejan por qué cada repositorio está presente
— Servlet Sinks y WebFlux Sinks para las dos formas de llamada vulnerables, Patched para
repositorios ya remediados upstream, Reference para Spring Security en sí y otros
no consumidores, Verified para el caso comprobado contra un servidor en ejecución, Gradle para
cobertura de herramientas de compilación, y Wide para la muestra masiva.
Espera, en los commits fijados:
| Resultado | |
|---|---|
| Tarjeta de actualización | 39 Major, 21 Minor, 6 Patch, 4 Completed (70 repositorios) |
| Tarjeta de seguridad | 65 repositorios expuestos |
| No aplicable | 5 repositorios no resuelven ninguna dependencia de Spring Security |
Cuatro de esos cinco realmente no usan Spring Security — spring-projects/spring-security
es la propia librería, JoeyBling/bootplus usa Apache Shiro, jenkinsci/stapler apunta
directamente a la API de servlet, y infofabrik/reportserver no tiene compilación Maven ni Gradle que
resolver. El quinto, xtuer/template-app, declara spring-security-web:5.0.0.RELEASE pero
su compilación Gradle no resuelve ninguna dependencia durante mod build, por lo que ninguna receta puede ver
la versión. Trátalo como no medido en lugar de no afectado.
Para aplicar la corrección y comprobar el resultado:
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 escribe en los checkouts en su lugar. Un check completo en todo el corpus es lento
y sacará a la luz fallos no relacionados con este cambio — toolchains JDK faltantes, repositorios
de dependencias inalcanzables, pruebas que ya estaban en rojo — por lo que la señal significativa es el
delta contra el mismo comando ejecutado antes de aplicar.
El análisis de contaminación de rewrite-program-analysis maneja el flujo de datos local y los resúmenes por método, por lo que estos patrones se detectan automáticamente:
String h = "Content-Length"; response.setIntHeader(h, 42); se marca — el framework rastrea la contaminación del literal a través de la asignación local.Map, List, colecciones personalizadas). stash.put("k", "Content-Length") seguido de response.setIntHeader(stash.get("k"), 42) no se detecta — la identidad put/get es opaca para el análisis.Moderne Propietaria. Solo para uso de clientes de Moderne bajo los términos de un contrato comercial.