
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 |