Sploitus

Exploit for Forced Browsing in Vmware Spring Security

githubexploit Β· 2026-09-11

Exploit Code

README242 lines
## https://sploitus.com/exploit?id=EEC836DD-F307-57D4-9FD8-3DFE8C849ECE
# 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.

| | |
|---|---|
| CVE | [CVE-2026-22732](https://spring.io/security/cve-2026-22732/) (CWE-425), published 2026-03-19 |
| CVSS 3.1 | 9.1 CRITICAL β€” `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N` |
| Direct dependency | `spring-boot-starter-web` + `spring-boot-starter-security` **2.7.18** |
| Vulnerable component | `spring-security-web` / `-config` / `-core` **5.7.11** β€” transitive only, never named in `pom.xml` |
| Fixed component | `spring-security-web` **5.7.14-0.cgr.2**, reached by one `` change β€” see [the transition](#the-vulnerable--patched-transition) |
| Verified on | Tomcat 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:

```
$ 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

```bash
./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:

```json
{"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

```
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)  >> 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:

```xml

  org.springframework.boot
  spring-boot-starter-parent
  2.7.18            
  2.7.18-0.cgr.3    

```

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:

| | 2.7.18 | 2.7.18-0.cgr.3 |
|---|---|---|
| `/safe/account` | OK 6/6 | OK 6/6 |
| `/vuln/stream/account` | OK 6/6 | OK 6/6 |
| `/vuln/flush/account` | OK 6/6 | OK 6/6 |
| `/vuln/content-length/account` | **BYPASSED 0/6** | **OK 6/6** |
| `/vuln/cache/account` | PARTIAL 4/6 | PARTIAL 4/6 (by design) |
| `exploit.sh` verdict | `VULNERABLE` | `PATCHED` |

### 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()`:

```java
@Override
public void setIntHeader(String name, int value) {
    checkContentLengthHeader(name, value);   // ` 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:

```xml

  9.0.118

```

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

```
$ 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:

| Property | Boot 2.7.18 default | Pinned here | Ceiling reason |
|---|---|---|---|
| `tomcat.version` | 9.0.83 | **9.0.118** | latest 9.0.x; 10+ is `jakarta.*` |
| `spring-framework.version` | 5.3.31 | **5.3.39** | last OSS 5.3.x on Central |
| `jackson-bom.version` | 2.13.5 | **2.22.2** | latest 2.x |
| `log4j2.version` | 2.17.2 | **2.26.1** | latest 2.x |
| `snakeyaml.version` | 1.30 | **1.33** | last 1.x; fix for the remaining CVE is 2.0 |
| `logback.version` | 1.2.12 | **1.2.13** | last 1.2.x β€” see below |
| `spring-security.version` | 5.7.11 | *left alone* | it is the subject of the demo |

**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

| State | Findings | Breakdown |
|---|---|---|
| Stock Boot 2.7.18 | 99 | 7C / 39H / 38M / 15L |
| + Tomcat bump | 65 | 3C / 23H / 29M / 10L |
| + all in-major bumps | **43** | **3C / 12H / 18M / 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:

```
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

| Component | Why 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 |

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

```
pom.xml                     parent + 2 starters, nothing else. Flip the  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](https://spring.io/security/cve-2026-22732/) β€” official advisory
- [GHSA-mf92-479x-3373](https://github.com/advisories/ghsa-mf92-479x-3373)
- [NVD CVE-2026-22732](https://nvd.nist.gov/vuln/detail/CVE-2026-22732)
- [Broadcom / Tanzu write-up](https://knowledge.broadcom.com/external/article/434435/cve202622732-spring-security-http-respon.html)
- [HeroDevs analysis](https://www.herodevs.com/blog-posts/cve-2026-22732-spring-security-silently-drops-http-security-headers)
- [semgrep/cve-2026-22732-demo](https://github.com/semgrep/cve-2026-22732-demo) β€” the reproduction whose stream/flush claims did not hold here
- [Red Hat Bugzilla #2449306](https://bugzilla.redhat.com/show_bug.cgi?id=2449306)