
OpenRewrite-Rezept, das die Unterdrückung von Spring-Security-Headern erkennt und behebt (CVE-2026-22732), indem es die missbräuchliche Verwendung des Content-Length-Headers identifiziert und eine Konfiguration zum sofortigen Schreiben von Headern generiert.
OpenRewrite-Rezept, das Code erkennt, der für CVE-2026-22732 anfällig ist – einen Spring-Security-Fehler, bei dem das Setzen von Content-Length über eine von drei Response-Methoden Spring Securitys OnCommittedResponseWrapper umgeht. Da der Wrapper den Header nie sieht, wird onResponseCommitted() nie ausgelöst, und die lazy hinzugefügten Security-Header (X-Frame-Options, X-Content-Type-Options, Cache-Control usw.) werden stillschweigend verworfen.
Die tatsächlichen Auslöser, bestätigt gegen verwundbares Spring Security 6.4.12 mit Spring Boot 3.4.3 / eingebettetem Tomcat:
Servlet Content-Length über die Wrapper-umgehenden Overloads
response.setHeader("Content-Length", "42");
response.setIntHeader("Content-Length", 42);
response.addIntHeader("Content-Length", 42);
Diese drei Overloads sind in OnCommittedResponseWrapper nicht überschrieben. Nachfolgende Body-Writes vervollständigen die deklarierte Länge und der Container committet, ohne den lazy Header-Writer auszulösen.
WebFlux Content-Length über HttpHeaders
serverHttpResponse.getHeaders().set("Content-Length", "42");
serverHttpResponse.getHeaders().add("Content-Length", "42");
serverHttpResponse.getHeaders().setContentLength(42L);
WebFlux unbedingte Response-Commits
serverHttpResponse.writeWith(Mono.just(dataBuffer));
serverHttpResponse.writeAndFlushWith(publisher);
serverHttpResponse.setComplete();
Das Rezept ist an das Vorhandensein von Spring Security gekoppelt – es erzeugt nichts in Dateien, die keinen org.springframework.security.*-Typ referenzieren – sowie an betroffene Spring-Security-Versionsbereiche. Laut der Spring-Advisory, veröffentlicht am 19.03.2026, sind die betroffenen Bereiche und Fix-Versionen:
| Serie | Betroffen | Behoben |
|---|---|---|
| 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) |
Projekte, die eine Spring-Security-Version auf oder über dem Fix ihrer Serie auflösen (oder auf einer zukünftigen Serie nach 7.0 / 6.5), gelten als nicht betroffen und erhalten keine Marker pro Sink oder pro Datei. Projekte, deren Version nicht aufgelöst werden kann, fallen auf die übliche musterbasierte Erkennung zurück, sodass ein Scanner eher einen Befund meldet, den er nicht widerlegen kann. Die Datentabelle SpringSecurityVersionByProject zeichnet weiterhin die aufgelöste Version auf und kennzeichnet jedes Projekt als betroffen oder nicht, sodass du nachvollziehen kannst, was gefiltert wurde.
Diese sehen gefährlich aus, werden aber vom Wrapper verfolgt, sodass Security-Header geschrieben werden, bevor die Response committet:
| Code | Warum es sicher ist |
|---|---|
response.setContentLength(int) / setContentLengthLong(long) | Überschrieben – der Wrapper zeichnet die deklarierte Länge auf und löst onResponseCommitted() aus, wenn der Body abgeschlossen ist. |
response.flushBuffer() | Überschrieben – ruft doOnResponseCommitted() vor super.flushBuffer() auf. |
response.getOutputStream().write(..) / flush() / close() | Gibt SaveContextServletOutputStream zurück; jeder write/flush/close löst doOnResponseCommitted() vor der Delegierung aus. |
response.getWriter().write(..) / print(..) / println(..) / flush() / close() | Gibt SaveContextPrintWriter zurück; gleiches Muster. |
response.addHeader("Content-Length", v) | Im Wrapper speziell behandelt – über setContentLength(long) geleitet. |
Der /vuln/flush-Endpunkt der Semgrep-Demo behauptet, flushBuffer() sei der Auslöser, aber auf einem verwundbaren Spring Security 6.4.12 gibt die Response tatsächlich alle sechs Security-Header zurück. Die echten Auslöser in der Demo sind die setIntHeader("Content-Length", ...)-Aufrufe in /vuln/stream und /vuln/content-length.
Führe dieses aus:
| Rezept | Zweck |
|---|---|
io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression | Führt jede Erkennung aus und erzeugt den Versionsbericht |
Der Aggregator oben besteht aus zwei kleineren Rezepten. Du kannst sie einzeln aufrufen, wenn du nur eine Erkennung möchtest.
| Rezept | Zweck |
|---|---|
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthHeader | Taint-Flow für das "Content-Length"-Literal, das setHeader / setIntHeader / addIntHeader (Servlet) oder HttpHeaders.set / add (WebFlux) erreicht |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | WebFlux unbedingte Commits: writeWith, writeAndFlushWith, setComplete, HttpHeaders.setContentLength |
Führe dieses aus:
| Rezept | Zweck |
|---|---|
io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression | Wählt die günstigste Abhilfe, die jedes Projekt tatsächlich umsetzen kann |
Es führt zwei Schritte in Reihenfolge aus.
1. Upgrade auf den Fix der eigenen Serie des Projekts. Spring Security hat den Fix als 6.5.9
und 7.0.4 auf Maven Central veröffentlicht. Jedes Upgrade ist an eine FindAffectedSpringSecuritySeries-
Vorbedingung gekoppelt, weil UpgradeDependencyVersion nur prüft, dass sein Ziel neuer ist – angewiesen
auf 7.0.4 würde es ein 5.8-Projekt bereitwillig über zwei Major-Versionen ziehen.
2. Füge eine eager Header-schreibende Konfiguration zu dem hinzu, was Schritt 1 nicht beheben konnte. Dies erzeugt
eine @Configuration-Klasse pro Projekt:
@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;
}
};
}
Das vorab Schreiben der Header macht es irrelevant, ob der Wrapper den Commit jemals beobachtet, sodass
dies jeden Sink im Projekt auf einmal schließt – einschließlich derer, die die Taint-Analyse nicht erreichen kann,
wie den Map-Flow unter „Bekannte Einschränkungen". Der BeanPostProcessor sieht Filter, die von der
HttpSecurity-DSL erstellt wurden, weil AutowireBeanFactoryObjectPostProcessor sie über die Bean-Factory
initialisiert. Zwei unabhängig veröffentlichte Fixes für diese CVE verwenden exakt diese Form
(hmcts/idam-web-public, und
armory-ios Spinnaker-Forks über den äquivalenten ObjectPostProcessor).
Schritt 2 deckt die Projekte ab, denen Schritt 1 nicht helfen kann:
| Situation | Warum das Upgrade nicht funktioniert |
|---|---|
| 5.7, 5.8, 6.3, 6.4 | Fix wird nur an Spring-Enterprise-Abonnenten ausgeliefert – nicht auf Maven Central |
| 6.0 - 6.2 | Für diese Serien wurde nie ein Fix veröffentlicht |
| Version durch ein importiertes BOM verwaltet | Es ist lokal nichts deklariert, das das Upgrade bearbeiten könnte |
Die letzte Zeile ist kein Randfall. nla/bamboo löst 7.0.3 – eine Serie mit Open-Source-
Fix – vollständig aus dem Spring-Boot-BOM auf, sodass eine reine Versionsprüfung es unter beiden Schritten
überspringen und verwundbar lassen würde. AddEagerHeaderWriterConfiguration weicht daher nur dann auf das Upgrade
aus, wenn das Projekt sowohl einen verfügbaren Open-Source-Fix hat als auch eine eigene Version deklariert.
Die erzeugte Klasse wird neben einer @EnableWebSecurity-Klasse platziert, wo eine existiert, mit Rückfall auf
@SpringBootApplication und dann auf jede @Configuration, sodass sie immer dort landet, wo die Komponentensuche
hinkommt. Projekte, die Header bereits eager schreiben oder einen gepatchten
OnCommittedResponseWrapper mitliefern (wie jogetworkflow/jw-community
es tut), werden in Ruhe gelassen.
| Rezept | Zweck |
|---|---|
io.moderne.recipe.cve202622732.UpgradeSpringSecurityToPatchedVersion | Serien-gekoppeltes Upgrade auf 6.5.9 / 7.0.4 |
io.moderne.recipe.cve202622732.AddEagerHeaderWriterConfiguration | Die erzeugte Konfiguration, für sich allein |
io.moderne.recipe.cve202622732.FindAffectedSpringSecuritySeries | Markiert Projekte auf einer betroffenen Serie; die Upgrade-Vorbedingung |
Angewendet auf semgrep/cve-2026-22732-demo auf
verwundbarem Spring Security 6.4.12, gegen eingebetteten Tomcat. Sein HeaderVerificationTest behauptet,
dass die Security-Header fehlen, sodass ein funktionierender Fix ihn scheitern lässt:
| Endpoint | Vorher | Nachher |
|---|---|---|
/vuln/stream | X-Content-Type-Options: null | nosniff |
/vuln/content-length | X-Content-Type-Options: null | nosniff |
/safe | nosniff | nosniff |
X-Frame-Options und Cache-Control folgen demselben Muster.
Über die 16 Repositorys des Korpus, die bauen (von 29 identifizierten), erzeugte der Fix eine Konfiguration für drei und ließ den Rest korrekt in Ruhe:
| Repository | Ergebnis |
|---|---|
semgrep/cve-2026-22732-demo | Erzeugt in com/example/vuln, neben @EnableWebSecurity; kompiliert, Header wiederhergestellt |
nla/bamboo | Erzeugt in ui/src/bamboo, neben @SpringBootApplication; kompiliert. Der BOM-verwaltete 7.0.3-Fall, den das Upgrade nicht erreichen kann |
star-whale/starwhale | Erzeugt in ai/starwhale/mlops/configuration/security, neben @EnableWebSecurity; kompiliert (JDK 11, sein deklariertes Ziel) |
hmcts/idam-web-public | In Ruhe gelassen – ruft bereits setShouldWriteHeadersEagerly auf |
okta/okta-idx-java (6.5.9), psi-probe (6.5.11), Ant-Media-Server (6.5.11) | In Ruhe gelassen – über dem Fix ihrer Serie |
apache/shenyu (6.3.1) | In Ruhe gelassen – nur reaktiv; HeaderWriterFilter hat keine Servlet-API dahinter, und die CVE betrifft nur Servlet |
brutusin/Brutusin-RPC (4.0.4) | In Ruhe gelassen – stammt aus der Zeit vor setShouldWriteHeadersEagerly (5.2) |
bootplus, template-app, front50, igor, rosco, spring-security, reportserver | In Ruhe gelassen – keine betroffene Spring-Security-Version aufgelöst |
Ein erneutes Ausführen des Fixes über die drei gepatchten Repositorys erzeugt nichts Weiteres, sodass die Abhilfe gegenüber ihrer eigenen Ausgabe auf echten Projekten idempotent ist.
Ein zweiter, breiterer Korpus zielt auf die Population ab, die der Fix tatsächlich adressiert – jede betroffene
Servlet-Spring-Security-Anwendung, da kein Sink erforderlich ist. Von 64 solcher Projekte, die per Codesuche gefunden
wurden, bauten 52, 48 lösten eine betroffene Version auf, und 38 wurden gepatcht; 35 davon kompilieren
(die anderen drei scheitern identisch ohne die erzeugte Datei). Alle 8 Projekte auf Spring Security unter 5.2 wurden
korrekt übersprungen. Siehe SUSCEPTIBLE-REPOSITORIES.md Abschnitt 8.
OnCommittedResponseWrapper extends jakarta.servlet.http.HttpServletResponseWrapper, sodass
CVE-2026-22732 nur Servlet betrifft und eine reaktive Anwendung ihm nicht ausgesetzt ist. Befunde von
FindHttpResponseContentLengthOrFlushBuffer kennzeichnen das analoge reaktive Muster und benötigen weiterhin
manuelle Prüfung, aber der Fix handelt bewusst nicht auf sie ein. AddEagerHeaderWriterConfiguration
überspringt jedes Modul, das HeaderWriterFilter ohne die Servlet-API dahinter sehen kann – der
Filter erweitert OncePerRequestFilter, und spring-security-web trägt die Servlet-API als
nicht-transitive provided-Abhängigkeit, sodass das Erzeugen dort mit
cannot access jakarta.servlet.Filter scheitert (beobachtet auf apache/shenyu).HeaderWriterFilter.setShouldWriteHeadersEagerly kommt in 5.2; auf 4.0.4 hat der Filter nur einen
Konstruktor und doFilterInternal. Die Erkennung meldet weiterhin EOL-Versionen, aber die Abhilfe wird
zurückgehalten, statt einen Aufruf zu erzeugen, der nicht kompilieren kann.| Tabelle | Zeilen |
|---|---|
TaintFlowTable (aus rewrite-program-analysis) | Eine Zeile pro Content-Length-Header-Taint-Treffer |
HttpResponseDirectCommitTable | Eine Zeile pro WebFlux-Struktur-Treffer |
SpringSecurityVersionByProject | Eine Zeile pro Projekt mit erkannter Spring-Security-Version |
Über die Moderne CLI:
mod run . --recipe io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression
Über 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 listet die 75 öffentlichen Repositorys auf, gegen die diese Rezepte entwickelt und gemessen
wurden, festgepinnt auf den exakten Commit, bei dem jedes ausgewertet wurde. Mehrere werden aktiv gepflegt
und upstream gepatcht, sodass die changeset-Spalte das ist, was die Zahlen unten
reproduzierbar statt nur plausibel macht.
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
Die Synchronisierung dauert etwa 20 Sekunden und 1,4 GB; der Build dauert ungefähr 15 Minuten und ist der
einzige langsame Schritt. mod devcenter schreibt devcenter.html in
corpus/.moderne/run/<runId>/, plus eines pro Organisations-Unterverzeichnis.
Die org1-Spalte teilt den Korpus in Gruppen, die widerspiegeln, warum jedes Repository vorhanden ist
– Servlet Sinks und WebFlux Sinks für die beiden verwundbaren Aufruf-Formen, Patched für
Repositorys, die bereits upstream behoben wurden, Reference für Spring Security selbst und andere
Nicht-Konsumenten, Verified für den Fall, der gegen einen laufenden Server geprüft wurde, Gradle für
Build-Tool-Abdeckung und Wide für die Massenstichprobe.
Erwarte, auf den gepinnten Commits:
| Ergebnis | |
|---|---|
| Upgrade-Karte | 39 Major, 21 Minor, 6 Patch, 4 Abgeschlossen (70 Repositorys) |
| Security-Karte | 65 exponierte Repositorys |
| Nicht anwendbar | 5 Repositorys lösen keine Spring-Security-Abhängigkeit auf |
Vier dieser fünf verwenden tatsächlich kein Spring Security – spring-projects/spring-security
ist die Bibliothek selbst, JoeyBling/bootplus verwendet Apache Shiro, jenkinsci/stapler zielt
direkt auf die Servlet-API ab, und infofabrik/reportserver hat keinen Maven- oder Gradle-Build zum
Auflösen. Das fünfte, xtuer/template-app, deklariert spring-security-web:5.0.0.RELEASE, aber
sein Gradle-Build löst während mod build überhaupt keine Abhängigkeiten auf, sodass kein Rezept die
Version sehen kann. Behandle es als ungemessen statt als nicht betroffen.
Um den Fix anzuwenden und das Ergebnis zu prüfen:
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 schreibt direkt in die Checkouts. Ein vollständiger check über den Korpus ist langsam
und wird Fehler zutage fördern, die nichts mit dieser Änderung zu tun haben – fehlende JDK-Toolchains, unerreichbare
Abhängigkeits-Repositorys, Tests, die bereits rot waren – sodass das aussagekräftige Signal die
Differenz gegenüber demselben Befehl ist, der vor dem Anwenden ausgeführt wurde.
Die Taint-Analyse aus rewrite-program-analysis behandelt lokalen Datenfluss und Zusammenfassungen pro Methode, sodass diese Muster automatisch erkannt werden:
String h = "Content-Length"; response.setIntHeader(h, 42); wird gekennzeichnet – das Framework verfolgt das Literal-Taint durch die lokale Zuweisung.Map, List, benutzerdefinierte Sammlungen). stash.put("k", "Content-Length") gefolgt von response.setIntHeader(stash.get("k"), 42) wird nicht erkannt – die put/get-Identität ist für die Analyse undurchsichtig.Moderne Proprietary. Nur zur Nutzung durch Moderne-Kunden unter den Bedingungen eines kommerziellen Vertrags.