Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Tools/GitHubGitHub/dylan-chainguard/cve-2026-22732-poc
Defensive ToolsVulnerability AnalysisExploitationSecurity VirtualizationWeb SecurityLearning & Education
GitHubdylan-chainguard/cve-2026-22732-poc

cve-2026-22732-poc

Proof-of-concept demonstrating CVE-2026-22732, a Spring Security flaw where setIntHeader("Content-Length") drops all security headers, with vulnerable and patched builds.

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
View Repository
2h 52m agoNot yet reviewed
Share

CVE-2026-22732 — Proof of Concept

Spring Security silently drops HTTP response security headers. Demo / educational use only; run it against nothing but this local app.

CVECVE-2026-22732 (CWE-425), published 2026-03-19
CVSS 3.19.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Direct dependencyspring-boot-starter-web + spring-boot-starter-security 2.7.18
Vulnerable componentspring-security-web / -config / -core 5.7.11 — transitive only, never named in pom.xml
Fixed componentspring-security-web 5.7.14-0.cgr.2, reached by one <version> change — see the transition
Verified onTomcat 9.0.118, JDK 17.0.18, macOS arm64

Affected ranges: 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 pins Spring Security 5.7.11, squarely inside the first range:

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

Run it

root@kitploit:~
./run.sh        # terminal 1 — builds and starts on :8080 (pins JDK 17)
./exploit.sh    # terminal 2 — drives every endpoint, diffs the headers

run.sh pins JAVA_HOME because Spring Boot 2.7.x cannot run on the JDK 25 that mvn resolves by default on this machine. Override with JAVA_HOME_17=/path/to/jdk17.

It also builds with -s settings-chainguard.xml by default, because the patched parent is not on Maven Central. Set MAVEN_SETTINGS=/path/to/your/settings.xml to point elsewhere, or MAVEN_SETTINGS= to build purely from Central — which works for stock 2.7.18 only.

exploit.sh reads the actual spring-security-web and spring-boot versions out of target/*.jar, so its banner always reports what is really running rather than a hardcoded string.

The setup

SecurityConfig applies no header customisation at all — Spring Security's defaults are in force, which is exactly what a security-conscious app relies on. Every endpoint returns the same sensitive body:

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

The only thing that varies is how the controller writes the response.

Measured results

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

Only /vuln/content-length/account changes state when the CVE is patched, so it is the only endpoint exploit.sh derives its verdict from. The rest are controls.

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

Three lines of ordinary-looking controller code strip every header Spring Security promised:

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

No X-Frame-Options, no X-Content-Type-Options, no Cache-Control, no Pragma, no Expires, no X-XSS-Protection. Compare /safe/account, which carries all six. The response is framable by any origin, MIME-sniffable, and cacheable — while serving a card number.

The by-design case — application-set cache header → cache suppression

Corrected: an earlier version of this README called this "exploit 2" and claimed it was the condition the advisory documents. It is not part of CVE-2026-22732 and no upgrade fixes it. CacheControlHeadersWriter is byte-identical in 5.7.11, 5.7.14-0.cgr.2, 6.5.8 (last vulnerable) and 6.5.9 (first fixed) — verified by diffing the sources jars. Its Javadoc states the behaviour outright: "Inserts headers to prevent caching if no cache control headers have been specified."

It is still worth demonstrating, because the leak is real and the residual risk survives patching. CacheControlHeadersWriter bails out if Cache-Control, Expires or Pragma is already present, so setting any one of the three suppresses all of Spring Security's no-store directives. One well-intentioned line does it:

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 counts as "present" in the table above, but with an attacker-friendly value the app chose — Spring Security's Expires: 0 was replaced, not merely dropped. Cardholder data is now storable by every browser and shared proxy on the path until 2099.

On the patched build /vuln/content-length/account flips to NOT cacheable, while /vuln/cache/account stays exactly as above. Only application code or a reverse proxy fixes it — which is the useful thing to say out loud in a demo: upgrading the library closes the CVE and leaves this untouched.

Two negative results, kept on purpose

Several widely-circulated write-ups — including a public reproduction repo — list response.getOutputStream() and response.flushBuffer() as triggers, explaining that "the response is committed before Spring Security can inject its headers". On Spring Security 5.7.11 that is wrong. Both endpoints deliver all six headers.

/diag/committed shows why the explanation doesn't hold. After a 12 KB write the response really is committed inside the controller, yet the headers still arrive:

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

OnCommittedResponseWrapper overrides flushBuffer() and the output-stream writes, so it gets its headers out ahead of those commits. Commit-ordering alone is not the bug; the declared-Content-Length path is. The Spring advisory itself never endorses the commit-ordering story.

Keeping these two endpoints in makes the PoC falsifiable: it shows what does not reproduce as clearly as what does, and both stay green across the patch, which is what makes the one endpoint that does flip meaningful.

The vulnerable → patched transition

One line in pom.xml, nothing else. No source change, no property change, no Spring Boot major bump:

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>

That re-pins spring-security.version from 5.7.11 to 5.7.14-0.cgr.2 (and spring-framework.version from 5.3.31 to 5.3.39-0.cgr.4) through the parent's spring-boot-dependencies. Measured:

The backport is the upstream fix

Diffing the spring-security-web sources jars, only one file matters. 5.7.11 → 5.7.14-0.cgr.2 adds setHeader / setIntHeader / addIntHeader overrides to OnCommittedResponseWrapper, each routing Content-Length through setContentLength():

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

Before the fix only addHeader did this, so setIntHeader("Content-Length", n) left the wrapper's tracked length at 0, onResponseCommitted() never fired, and HeaderWriterFilter never wrote its headers before Tomcat committed the response.

The same hunk appears verbatim when diffing upstream 6.5.8 (last vulnerable) against 6.5.9 (first fixed), so this is the official fix backported, not a reimplementation. The Chainguard build adds two null-guards upstream 6.5.9 does not have (value != null on the String overload, and (csq != null) ? csq.length() : 4 in append).

Other mitigations

Spring Security 5.7.x is end-of-life upstream; OSS fixes land only in 6.4.15 / 6.5.9 / 7.0.4+. If a rebuilt 5.7.x is not an option:

  1. Upgrade off 5.7.x — a Spring Boot 3.x migration.
  2. Workaround — set HeaderWriterFilter.shouldWriteHeadersEagerly = true via an ObjectPostProcessor. Per the advisory this changes behaviour: application-written headers then override only specific headers rather than suppressing Spring Security's. This one also fixes /vuln/cache/account, which the version bump does not.
  3. Commercial support — Tanzu Spring Enterprise backports for 5.7.x/5.8.x.
  4. Defence in depth — set the headers at the reverse proxy / ingress so a dropped application header is not the only control. This is the only listed option that covers both endpoints.

None of these are wired into this project, so the vulnerable behaviour is the default and the patched state is reachable by the single <version> change above.

Patching the embedded Tomcat without upgrading Spring Boot

Boot 2.7.18 pins Tomcat 9.0.83, which grype . flags with 34 CVEs (4 Critical). All of them are fixed at 9.0.118 or below, and 9.0.118 is the newest 9.0.x release — so one property clears the set:

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

spring-boot-dependencies declares every tomcat-embed-* artifact through that single property, so overriding it re-pins core, el and websocket together. Verified:

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

Constraint: stay on the 9.0.x line. Tomcat 10+ moved the Servlet API to jakarta.* while Spring Framework 5.3 compiles against javax.servlet, so a 10.x/11.x bump fails at runtime with NoClassDefFoundError on the servlet types.

Patching everything else, still within each major line

Same mechanism applied to the rest of Boot 2.7.18's managed dependencies. No major-version bumps, and no Spring Boot upgrade:

These overrides interact with the patched parent, so know what they do before demoing. On 2.7.18-0.cgr.3 the parent already supplies tomcat.version 9.0.118, logback.version 1.2.13 and snakeyaml.version 1.33 — those three rows become exact duplicates and can be deleted without changing anything. The jackson-bom.version and log4j2.version rows still do real work: the patched parent keeps Boot's stock 2.13.5 / 2.17.2, so the overrides win and those two dependencies resolve to plain upstream builds rather than Chainguard-built ones. spring-framework.version is commented out on purpose, which is what lets the parent's 5.3.39-0.cgr.4 through.

grype . progression

StateFindingsBreakdown
Stock Boot 2.7.18997C / 39H / 38M / 15L
+ Tomcat bump653C / 23H / 29M / 10L

Fully cleared: Tomcat (34), Jackson (7), log4j (1). Overall 99 → 43, Highs 39 → 12.

Verified after every bump: the app boots on Tomcat/9.0.118, all six endpoints return 200, and the CVE reproduces byte-for-byte. Spring Boot is still 2.7.18 and Spring Security still 5.7.11, so CVE-2026-22732 is untouched — which is the point of this section, and also its honest limit: patching everything around it does nothing for the application-framework CVE. Fixing that one needs the parent bump, not a property.

Why Logback stops at 1.2.13

The latest 1.x is 1.6.3 — same major, so nominally in scope. It does not work. Logback 1.3+ replaced the SLF4J 1.7 StaticLoggerBinder with the SLF4J 2.x ServiceLoader provider, and Boot 2.7's LogbackLoggingSystem calls StaticLoggerBinder directly. Measured with 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)

Escaping this needs SLF4J 2.x (a major bump) and a Boot 3.x logging system. 1.2.13 is the real ceiling, leaving 6 Logback findings (2 Medium, 4 Low) unfixable on this line.

The 43 that remain

Two of the three remaining Criticals are worth reading properly rather than by score:

  • CVE-2016-1000027 (spring-web, fix 6.0.0) — deserialization via HttpInvokerServiceExporter. This app does not use HTTP Invoker, so it is not reachable here.
  • CVE-2024-38821 (spring-security-web, fix 5.7.13) — static-resource auth bypass in WebFlux. This is a servlet app, so also not reachable. It is fixable in-major (5.7.13/5.7.14 are on Central) and was left only to keep this section pinned at 5.7.11. The patched parent clears it as a side effect, since 5.7.14-0.cgr.2 is past the fix version.
  • CVE-2026-22732 — intentional in the vulnerable state; cleared by the parent bump.

The residue is structural: Spring Framework 5.3.x and Spring Security 5.7.x are both EOL. That, not Tomcat or Jackson, is the real argument for a Boot 3.x migration.

Layout

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

Auth is permitAll and CSRF is off so curl works unauthenticated — neither is part of this CVE.

Sources

  • spring.io/security/cve-2026-22732 — official advisory
  • GHSA-mf92-479x-3373
  • NVD CVE-2026-22732
  • Broadcom / Tanzu write-up
  • HeroDevs analysis
  • semgrep/cve-2026-22732-demo — the reproduction whose stream/flush claims did not hold here
  • Red Hat Bugzilla #2449306
Download Tool
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
PropertyBoot 2.7.18 defaultPinned hereCeiling reason
tomcat.version9.0.839.0.118latest 9.0.x; 10+ is jakarta.*
spring-framework.version5.3.315.3.39last OSS 5.3.x on Central
jackson-bom.version2.13.52.22.2latest 2.x
log4j2.version2.17.22.26.1latest 2.x
snakeyaml.version1.301.33last 1.x; fix for the remaining CVE is 2.0
logback.version1.2.121.2.13last 1.2.x — see below
spring-security.version5.7.11left aloneit is the subject of the demo
+ all in-major bumps
43
3C / 12H / 18M / 10L
ComponentWhy it can't be fixed in-major
spring-webmvc / -expression / -core / -context (25)5.3.39 is the last OSS 5.3.x; 14 of 15 webmvc findings have no fix at all, and the 5.3.42 grype cites is commercial-only
logback-core (6)needs SLF4J 2.x, see above
spring-security-* (8)EOL line; CVE-2026-22732 is deliberate
spring-boot / -autoconfigure (3)no fix published for 2.7.x
snakeyaml (1)CVE-2022-1471 is fixed only in 2.0