Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
Herramientas/GitHubGitHub/dylan-chainguard/cve-2026-22732-poc
Herramientas DefensivasAnálisis de VulnerabilidadesExplotaciónVirtualización de SeguridadSeguridad WebAprendizaje y Educación
GitHubdylan-chainguard/cve-2026-22732-poc

cve-2026-22732-poc

Prueba de concepto que demuestra CVE-2026-22732, una falla de Spring Security donde setIntHeader("Content-Length") elimina todos los encabezados de seguridad, con compilaciones vulnerables y parcheadas.

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Ver Repositorio
hace 2h 52mAún no revisado
Compartir

CVE-2026-22732 — Prueba de concepto

Spring Security descarta silenciosamente las cabeceras de seguridad de las respuestas HTTP. Solo para uso demostrativo/educativo; ejecútalo únicamente contra esta aplicación local.

CVECVE-2026-22732 (CWE-425), publicado el 2026-03-19
CVSS 3.19.1 CRÍTICO — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Dependencia directaspring-boot-starter-web + spring-boot-starter-security 2.7.18
Componente vulnerablespring-security-web / -config / -core 5.7.11 — solo transitivo, nunca declarado en pom.xml
Componente corregidospring-security-web 5.7.14-0.cgr.2, alcanzado con un solo cambio de <version> — véase la transición
Verificado enTomcat 9.0.118, JDK 17.0.18, macOS arm64

Rangos afectados: 5.7.0–5.7.21, 5.8.0–5.8.23, 6.3.0–6.3.14, 6.4.0–6.4.14, 6.5.0–6.5.8, 7.0.0–7.0.3. Spring Boot 2.7.18 fija Spring Security 5.7.11, justo dentro del primer rango:

root@kitploit:~
$ mvn dependency:tree -Dincludes=org.springframework.security
+- org.springframework.security:spring-security-config:jar:5.7.11:compile
|  \- org.springframework.security:spring-security-core:jar:5.7.11:compile
\- org.springframework.security:spring-security-web:jar:5.7.11:compile

Ejecútalo

root@kitploit:~
./run.sh        # terminal 1 — compila y arranca en :8080 (fija JDK 17)
./exploit.sh    # terminal 2 — recorre todos los endpoints, compara las cabeceras

run.sh fija JAVA_HOME porque Spring Boot 2.7.x no puede ejecutarse sobre el JDK 25 que mvn resuelve por defecto en esta máquina. Sobrescríbelo con JAVA_HOME_17=/path/to/jdk17.

También compila con -s settings-chainguard.xml por defecto, porque el parent parcheado no está en Maven Central. Establece MAVEN_SETTINGS=/path/to/your/settings.xml para apuntar a otro sitio, o MAVEN_SETTINGS= para compilar únicamente desde Central — lo cual solo funciona para el 2.7.18 original.

exploit.sh lee las versiones reales de spring-security-web y spring-boot desde target/*.jar, de modo que su banner siempre informa de lo que realmente se está ejecutando en lugar de una cadena codificada.

La configuración

SecurityConfig no aplica ninguna personalización de cabeceras en absoluto — los valores por defecto de Spring Security están en vigor, que es exactamente en lo que confía una aplicación consciente de la seguridad. Cada endpoint devuelve el mismo cuerpo sensible:

root@kitploit:~
{"account":"4111-1111-1111-1111","holder":"D. Havelock","balance":"82914.55"}

Lo único que varía es cómo el controlador escribe la respuesta.

Resultados medidos

root@kitploit:~
BASELINE  standard Spring MVC return value
  /safe/account                    OK        all 6 headers delivered

CONTROL   getOutputStream(), body > 8 KB buffer
  /vuln/stream/account             OK        all 6 headers delivered

CONTROL   explicit response.flushBuffer()
  /vuln/flush/account              OK        all 6 headers delivered

EXPLOIT   setIntHeader("Content-Length", n)  <-- CVE-2026-22732
  /vuln/content-length/account     BYPASSED  ALL 6 security headers dropped

BY DESIGN application sets its own Expires header (NOT this CVE)
  /vuln/cache/account              PARTIAL   Cache-Control + Pragma dropped

Solo /vuln/content-length/account cambia de estado cuando se parchea la CVE, por lo que es el único endpoint del que exploit.sh deriva su veredicto. El resto son controles.

La CVE — setIntHeader("Content-Length", n) → bypass total

Tres líneas de código de controlador de aspecto corriente eliminan todas las cabeceras que Spring Security prometía:

root@kitploit:~
response.setContentType("application/json");
response.setIntHeader("Content-Length", body.length);
response.getOutputStream().write(body);
root@kitploit:~
$ curl -sD - -o /dev/null http://localhost:8080/vuln/content-length/account
HTTP/1.1 200
Content-Type: application/json
Content-Length: 77
Date: Tue, 01 Sep 2026 00:53:18 GMT

Sin X-Frame-Options, sin X-Content-Type-Options, sin Cache-Control, sin Pragma, sin Expires, sin X-XSS-Protection. Compárese con /safe/account, que las lleva las seis. La respuesta es enmarcable desde cualquier origen, susceptible de sniffing de MIME y cacheable — mientras sirve un número de tarjeta.

El caso por diseño — cabecera de caché fijada por la aplicación → supresión de caché

Corregido: una versión anterior de este README llamaba a esto "exploit 2" y afirmaba que era la condición que documenta el aviso. No forma parte de CVE-2026-22732 y ninguna actualización lo corrige. CacheControlHeadersWriter es idéntico byte a byte en 5.7.11, 5.7.14-0.cgr.2, 6.5.8 (último vulnerable) y 6.5.9 (primero corregido) — verificado comparando los jars de fuentes. Su Javadoc declara el comportamiento sin rodeos: "Inserts headers to prevent caching if no cache control headers have been specified."

Aun así merece la pena demostrarlo, porque la fuga es real y el riesgo residual sobrevive al parcheo. CacheControlHeadersWriter se detiene si Cache-Control, Expires o Pragma ya están presentes, de modo que fijar una cualquiera de las tres suprime todas las directivas no-store de Spring Security. Una línea bienintencionada lo consigue:

root@kitploit:~
response.setHeader("Expires", "Thu, 01 Jan 2099 00:00:00 GMT");
root@kitploit:~
/safe/account                  NOT cacheable  (Cache-Control: no-cache, no-store, max-age=0, must-revalidate)
/vuln/cache/account            CACHEABLE      (Cache-Control: absent / Expires: Thu, 01 Jan 2099 00:00:00 GMT)
/vuln/content-length/account   CACHEABLE      (Cache-Control: absent / Expires absent)

Expires cuenta como "presente" en la tabla anterior, pero con un valor favorable al atacante elegido por la aplicación — el Expires: 0 de Spring Security fue reemplazado, no simplemente descartado. Los datos del titular de la tarjeta ahora son almacenables por todos los navegadores y proxies compartidos en la ruta hasta 2099.

En la compilación parcheada /vuln/content-length/account pasa a NOT cacheable, mientras que /vuln/cache/account permanece exactamente como arriba. Solo el código de la aplicación o un proxy inverso lo corrige — que es lo útil que hay que decir en voz alta en una demo: actualizar la biblioteca cierra la CVE y deja esto intacto.

Dos resultados negativos, conservados a propósito

Varios artículos ampliamente difundidos — incluido un repositorio público de reproducción — listan response.getOutputStream() y response.flushBuffer() como desencadenantes, explicando que "la respuesta se confirma antes de que Spring Security pueda inyectar sus cabeceras". En Spring Security 5.7.11 eso es incorrecto. Ambos endpoints entregan las seis cabeceras.

/diag/committed muestra por qué la explicación no se sostiene. Tras una escritura de 12 KB la respuesta realmente está confirmada dentro del controlador, y aun así las cabeceras llegan:

root@kitploit:~
>>> DIAG response.isCommitted() after 12048 byte write = true
    | wrapper class = org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterResponse

OnCommittedResponseWrapper sobrescribe flushBuffer() y las escrituras del flujo de salida, de modo que emite sus cabeceras antes de esas confirmaciones. El orden de confirmación por sí solo no es el fallo; la ruta de Content-Length declarado sí lo es. El propio aviso de Spring nunca respalda la historia del orden de confirmación.

Mantener estos dos endpoints hace que la PoC sea falsable: muestra lo que no se reproduce con la misma claridad que lo que sí, y ambos permanecen en verde a través del parche, que es lo que hace que el único endpoint que sí cambia sea significativo.

La transición de vulnerable → parcheado

Una línea en pom.xml, nada más. Sin cambios en el código fuente, sin cambios de propiedades, sin salto de versión mayor de Spring Boot:

root@kitploit:~
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.7.18</version>            <!-- vulnerable -->
  <version>2.7.18-0.cgr.3</version>    <!-- patched   -->
</parent>

Eso vuelve a fijar spring-security.version de 5.7.11 a 5.7.14-0.cgr.2 (y spring-framework.version de 5.3.31 a 5.3.39-0.cgr.4) a través del spring-boot-dependencies del parent. Medido:

2.7.182.7.18-0.cgr.3
/safe/accountOK 6/6OK 6/6
/vuln/stream/accountOK 6/6OK 6/6
/vuln/flush/accountOK 6/6OK 6/6
/vuln/content-length/accountBYPASSED 0/6OK 6/6
/vuln/cache/accountPARTIAL 4/6PARTIAL 4/6 (by design)
exploit.sh verdictVULNERABLEPATCHED

El backport es la corrección upstream

Comparando los jars de fuentes de spring-security-web, solo importa un archivo. 5.7.11 → 5.7.14-0.cgr.2 añade las sobrescrituras de setHeader / setIntHeader / addIntHeader a OnCommittedResponseWrapper, cada una enrutando Content-Length a través de setContentLength():

root@kitploit:~
@Override
public void setIntHeader(String name, int value) {
    checkContentLengthHeader(name, value);   // <-- added
    super.setIntHeader(name, value);
}

Antes de la corrección solo addHeader hacía esto, de modo que setIntHeader("Content-Length", n) dejaba la longitud rastreada del wrapper en 0, onResponseCommitted() nunca se disparaba, y HeaderWriterFilter nunca escribía sus cabeceras antes de que Tomcat confirmara la respuesta.

El mismo fragmento aparece literalmente al comparar el upstream 6.5.8 (último vulnerable) con 6.5.9 (primero corregido), así que esta es la corrección oficial portada, no una reimplementación. La compilación de Chainguard añade dos comprobaciones de nulos que el upstream 6.5.9 no tiene (value != null en la sobrecarga de String, y (csq != null) ? csq.length() : 4 en append).

Otras mitigaciones

Spring Security 5.7.x está al final de su vida útil en upstream; las correcciones OSS llegan solo en 6.4.15 / 6.5.9 / 7.0.4+. Si una 5.7.x recompilada no es una opción:

  1. Actualizar fuera de 5.7.x — una migración a Spring Boot 3.x.
  2. Workaround — establecer HeaderWriterFilter.shouldWriteHeadersEagerly = true mediante un ObjectPostProcessor. Según el aviso esto cambia el comportamiento: las cabeceras escritas por la aplicación entonces sobrescriben solo cabeceras específicas en lugar de suprimir las de Spring Security. Este también corrige /vuln/cache/account, cosa que el salto de versión no hace.
  3. Soporte comercial — backports de Tanzu Spring Enterprise para 5.7.x/5.8.x.
  4. Defensa en profundidad — establecer las cabeceras en el proxy inverso / ingress para que una cabecera de aplicación descartada no sea el único control. Esta es la única opción listada que cubre ambos endpoints.

Ninguna de estas está integrada en este proyecto, por lo que el comportamiento vulnerable es el predeterminado y el estado parcheado es alcanzable con el único cambio de <version> anterior.

Parchear el Tomcat embebido sin actualizar Spring Boot

Boot 2.7.18 fija Tomcat 9.0.83, que grype . marca con 34 CVEs (4 Críticas). Todas ellas están corregidas en 9.0.118 o inferior, y 9.0.118 es la versión más reciente de la línea 9.0.x — así que una sola propiedad limpia el conjunto:

root@kitploit:~
<properties>
  <tomcat.version>9.0.118</tomcat.version>
</properties>

spring-boot-dependencies declara cada artefacto tomcat-embed-* a través de esa única propiedad, de modo que sobrescribirla vuelve a fijar core, el y websocket juntos. Verificado:

root@kitploit:~
$ mvn dependency:tree | grep tomcat-embed
tomcat-embed-core:jar:9.0.118:compile
tomcat-embed-el:jar:9.0.118:compile
tomcat-embed-websocket:jar:9.0.118:compile

Restricción: mantente en la línea 9.0.x. Tomcat 10+ movió la API de Servlet a jakarta.* mientras que Spring Framework 5.3 compila contra javax.servlet, por lo que un salto a 10.x/11.x falla en tiempo de ejecución con NoClassDefFoundError en los tipos de servlet.

Parchear todo lo demás, aún dentro de cada línea mayor

El mismo mecanismo aplicado al resto de las dependencias gestionadas de Boot 2.7.18. Sin saltos de versión mayor, y sin actualización de Spring Boot:

PropiedadValor por defecto de Boot 2.7.18Fijado aquíMotivo del techo
tomcat.version9.0.839.0.118última 9.0.x; 10+ es jakarta.*
spring-framework.version5.3.315.3.39última 5.3.x OSS en Central
jackson-bom.version2.13.52.22.2última 2.x
log4j2.version2.17.22.26.1última 2.x
snakeyaml.version1.301.33última 1.x; la corrección para la CVE restante es 2.0
logback.version1.2.121.2.13última 1.2.x — véase más abajo
spring-security.version5.7.11sin tocares el objeto de la demo

Estas sobrescrituras interactúan con el parent parcheado, así que conoce lo que hacen antes de hacer la demo. En 2.7.18-0.cgr.3 el parent ya proporciona tomcat.version 9.0.118, logback.version 1.2.13 y snakeyaml.version 1.33 — esas tres filas se vuelven duplicados exactos y pueden eliminarse sin cambiar nada. Las filas de jackson-bom.version y log4j2.version siguen haciendo trabajo real: el parent parcheado mantiene los valores originales de Boot 2.13.5 / 2.17.2, así que las sobrescrituras ganan y esas dos dependencias se resuelven a compilaciones upstream normales en lugar de las construidas por Chainguard. spring-framework.version está comentada a propósito, que es lo que deja pasar el 5.3.39-0.cgr.4 del parent.

Progresión de grype .

EstadoHallazgosDesglose
Boot 2.7.18 original997C / 39H / 38M / 15L
+ salto de Tomcat653C / 23H / 29M / 10L
+ todos los saltos dentro de la línea mayor433C / 12H / 18M / 10L

Totalmente limpiados: Tomcat (34), Jackson (7), log4j (1). En conjunto 99 → 43, Altas 39 → 12.

Verificado tras cada salto: la aplicación arranca en Tomcat/9.0.118, los seis endpoints devuelven 200, y la CVE se reproduce byte a byte. Spring Boot sigue siendo 2.7.18 y Spring Security sigue siendo 5.7.11, así que CVE-2026-22732 no se toca — que es el objetivo de esta sección, y también su límite honesto: parchear todo lo que la rodea no hace nada por la CVE del framework de aplicación. Corregir esa requiere el salto del parent, no una propiedad.

Por qué Logback se detiene en 1.2.13

La última 1.x es 1.6.3 — misma versión mayor, así que nominalmente dentro del alcance. No funciona. Logback 1.3+ reemplazó el StaticLoggerBinder de SLF4J 1.7 por el proveedor ServiceLoader de SLF4J 2.x, y el LogbackLoggingSystem de Boot 2.7 llama directamente a StaticLoggerBinder. Medido con 1.5.38:

root@kitploit:~
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder"
Caused by: java.lang.NoClassDefFoundError: org/slf4j/impl/StaticLoggerBinder
    at org.springframework.boot.logging.logback.LogbackLoggingSystem.getLoggerContext(...:304)

Escapar de esto requiere SLF4J 2.x (un salto de versión mayor) y un sistema de logging de Boot 3.x. 1.2.13 es el techo real, dejando 6 hallazgos de Logback (2 Medios, 4 Bajos) imposibles de corregir en esta línea.

Los 43 que quedan

ComponentePor qué no puede corregirse dentro de la línea mayor
spring-webmvc / -expression / -core / -context (25)5.3.39 es la última 5.3.x OSS; 14 de 15 hallazgos de webmvc no tienen corrección alguna, y la 5.3.42 que cita grype es solo comercial
logback-core (6)necesita SLF4J 2.x, véase arriba
spring-security-* (8)línea EOL; CVE-2026-22732 es deliberada
spring-boot / -autoconfigure (3)no hay corrección publicada para 2.7.x
snakeyaml (1)CVE-2022-1471 solo se corrige en 2.0

Dos de las tres Críticas restantes merecen leerse bien en lugar de por puntuación:

  • CVE-2016-1000027 (spring-web, corrección 6.0.0) — deserialización mediante HttpInvokerServiceExporter. Esta aplicación no usa HTTP Invoker, así que no es alcanzable aquí.
  • CVE-2024-38821 (spring-security-web, corrección 5.7.13) — bypass de autenticación de recursos estáticos en WebFlux. Esta es una aplicación de servlets, así que tampoco es alcanzable. Sí es corregible dentro de la línea mayor (5.7.13/5.7.14 están en Central) y se dejó solo para mantener esta sección fijada en 5.7.11. El parent parcheado la limpia como efecto secundario, ya que 5.7.14-0.cgr.2 supera la versión de corrección.
  • CVE-2026-22732 — intencional en el estado vulnerable; limpiada por el salto del parent.

El residuo es estructural: Spring Framework 5.3.x y Spring Security 5.7.x están ambos EOL. Eso, no Tomcat ni Jackson, es el verdadero argumento para una migración a Boot 3.x.

Estructura

root@kitploit:~
pom.xml                     parent + 2 starters, nothing else. Flip the <version> to switch state.
run.sh                      build + run on JDK 17, via settings-chainguard.xml
exploit.sh                  header diff, cache impact, clickjacking check, CVE-scoped verdict
settings-chainguard.xml     Chainguard Libraries repo -- required for the patched parent
src/main/java/com/example/poc/
  PocApplication.java       @SpringBootApplication
  SecurityConfig.java       permitAll, zero header customisation
  AccountController.java    baseline, 2 exploits, 2 controls, 1 diagnostic

La autenticación es permitAll y CSRF está desactivado para que curl funcione sin autenticar — ninguna de las dos cosas forma parte de esta CVE.

Fuentes

  • spring.io/security/cve-2026-22732 — aviso oficial
  • GHSA-mf92-479x-3373
  • NVD CVE-2026-22732
  • Artículo de Broadcom / Tanzu
  • Análisis de HeroDevs
  • semgrep/cve-2026-22732-demo — la reproducción cuyas afirmaciones sobre stream/flush no se sostuvieron aquí
  • Red Hat Bugzilla #2449306
Descargar herramienta