
Ricetta OpenRewrite che rileva e corregge la soppressione degli header di Spring Security (CVE-2026-22732) identificando l'uso improprio dell'header Content-Length e generando una configurazione di scrittura anticipata degli header.
Ricetta OpenRewrite che rileva codice vulnerabile a CVE-2026-22732, un difetto di Spring Security in cui l'impostazione di Content-Length tramite uno dei tre metodi di risposta bypassa OnCommittedResponseWrapper di Spring Security. Poiché il wrapper non vede mai l'header, onResponseCommitted() non viene mai attivato e gli header di sicurezza aggiunti in modo lazy (X-Frame-Options, X-Content-Type-Options, Cache-Control, ecc.) vengono silenziosamente eliminati.
I trigger effettivi, confermati contro Spring Security 6.4.12 vulnerabile con Spring Boot 3.4.3 / Tomcat embedded:
Content-Length servlet tramite gli overload che bypassano il wrapper
response.setHeader("Content-Length", "42");
response.setIntHeader("Content-Length", 42);
response.addIntHeader("Content-Length", 42);
Questi tre overload non sono sovrascritti in OnCommittedResponseWrapper. Le successive scritture del body completano la lunghezza dichiarata e il container committa senza attivare il writer di header lazy.
Content-Length WebFlux tramite HttpHeaders
serverHttpResponse.getHeaders().set("Content-Length", "42");
serverHttpResponse.getHeaders().add("Content-Length", "42");
serverHttpResponse.getHeaders().setContentLength(42L);
Commit di risposta WebFlux incondizionati
serverHttpResponse.writeWith(Mono.just(dataBuffer));
serverHttpResponse.writeAndFlushWith(publisher);
serverHttpResponse.setComplete();
La ricetta è condizionata alla presenza di Spring Security — non emette nulla nei file che non referenziano alcun tipo org.springframework.security.* — e alle versioni di Spring Security interessate. Secondo l'avviso Spring pubblicato il 2026-03-19, gli intervalli interessati e le versioni corrette sono:
| Serie | Interessate | Corretta |
|---|---|---|
| 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) |
I progetti che risolvono una versione di Spring Security uguale o superiore alla correzione nella propria serie (o in qualsiasi serie futura oltre 7.0 / 6.5) sono trattati come non interessati e non ricevono marcatori per-sink o per-file. I progetti in cui la versione non può essere risolta ricadono nella consueta rilevazione basata su pattern, così uno scanner tende a segnalare un risultato che non può smentire. La tabella dati SpringSecurityVersionByProject registra comunque la versione risolta e contrassegna ogni progetto come interessato o meno, così puoi verificare cosa è stato filtrato.
Questi sembrano pericolosi ma sono tracciati dal wrapper, quindi gli header di sicurezza vengono scritti prima del commit della risposta:
| Codice | Perché è sicuro |
|---|---|
response.setContentLength(int) / setContentLengthLong(long) | Sovrascritti — il wrapper registra la lunghezza dichiarata e attiva onResponseCommitted() quando il body è completo. |
response.flushBuffer() | Sovrascritto — chiama doOnResponseCommitted() prima di super.flushBuffer(). |
response.getOutputStream().write(..) / flush() / close() | Restituisce SaveContextServletOutputStream; ogni write/flush/close attiva doOnResponseCommitted() prima di delegare. |
response.getWriter().write(..) / print(..) / println(..) / flush() / close() | Restituisce SaveContextPrintWriter; stesso pattern. |
response.addHeader("Content-Length", v) | Caso speciale nel wrapper — instradato tramite setContentLength(long). |
L'endpoint /vuln/flush della demo Semgrep afferma che flushBuffer() è il trigger, ma su Spring Security 6.4.12 vulnerabile la risposta restituisce effettivamente tutti e sei gli header di sicurezza. I veri trigger nella demo sono le chiamate setIntHeader("Content-Length", ...) in /vuln/stream e /vuln/content-length.
Esegui questa:
| Ricetta | Scopo |
|---|---|
io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression | Esegue ogni rilevazione ed emette la tabella del report delle versioni |
L'aggregatore sopra è composto da due ricette più piccole. Puoi invocarle singolarmente se vuoi una sola rilevazione.
| Ricetta | Scopo |
|---|---|
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthHeader | Flusso di taint per il letterale "Content-Length" che raggiunge setHeader / setIntHeader / addIntHeader (servlet) o HttpHeaders.set / add (WebFlux) |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | Commit WebFlux incondizionati: writeWith, writeAndFlushWith, setComplete, HttpHeaders.setContentLength |
Esegui questa:
| Ricetta | Scopo |
|---|---|
io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression | Sceglie la rimedazione più economica che ogni progetto può effettivamente adottare |
Esegue due passaggi in ordine.
1. Aggiornamento alla correzione sulla serie del progetto. Spring Security ha pubblicato la correzione come 6.5.9
e 7.0.4 su Maven Central. Ogni aggiornamento è condizionato da una precondizione FindAffectedSpringSecuritySeries,
perché UpgradeDependencyVersion controlla solo che il suo target sia più recente — se gli si dicesse di
andare a 7.0.4 trascinerebbe volentieri un progetto 5.8 attraverso due versioni major.
2. Aggiunta di una configurazione eager per la scrittura degli header a qualunque cosa il passaggio 1 non abbia potuto correggere. Questo genera
una classe @Configuration per progetto:
@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;
}
};
}
Scrivere gli header in anticipo rende irrilevante se il wrapper osservi mai il commit, quindi
questo chiude tutti i sink del progetto in una volta — inclusi quelli che l'analisi del taint non può raggiungere,
come il flusso Map in "Limitazioni note". Il BeanPostProcessor vede i filter costruiti dal
DSL HttpSecurity perché AutowireBeanFactoryObjectPostProcessor li inizializza tramite la
bean factory. Due correzioni pubblicate indipendentemente per questa CVE usano esattamente questa forma
(hmcts/idam-web-public, e
i fork Spinnaker di armory-io tramite l'equivalente ObjectPostProcessor).
Il passaggio 2 è ciò che copre i progetti che il passaggio 1 non può aiutare:
| Situazione | Perché l'aggiornamento non funziona |
|---|---|
| 5.7, 5.8, 6.3, 6.4 | La correzione è disponibile solo per gli abbonati Spring Enterprise — non su Maven Central |
| 6.0 - 6.2 | Nessuna correzione è mai stata rilasciata su quelle serie |
| Versione gestita da un BOM importato | Non è dichiarato nulla localmente che l'aggiornamento possa modificare |
L'ultima riga non è un caso limite. nla/bamboo risolve 7.0.3 — una serie con una correzione
open-source — interamente dal BOM Spring Boot, quindi un semplice controllo della versione lo salterebbe in entrambi i passaggi
lasciandolo vulnerabile. AddEagerHeaderWriterConfiguration quindi rimanda all'aggiornamento solo
quando il progetto ha sia una correzione open-source disponibile sia dichiara una versione propria.
La classe generata viene posizionata accanto a una classe @EnableWebSecurity dove esiste, con fallback
a @SpringBootApplication e poi a qualsiasi @Configuration, così finisce sempre dove la scansione dei componenti
la raggiunge. I progetti che già scrivono gli header in modo eager, o che forniscono una versione patchata di
OnCommittedResponseWrapper (come fa jogetworkflow/jw-community),
vengono lasciati invariati.
| Ricetta | Scopo |
|---|---|
io.moderne.recipe.cve202622732.UpgradeSpringSecurityToPatchedVersion | Aggiornamento condizionato per serie a 6.5.9 / 7.0.4 |
io.moderne.recipe.cve202622732.AddEagerHeaderWriterConfiguration | La configurazione generata, da sola |
io.moderne.recipe.cve202622732.FindAffectedSpringSecuritySeries | Contrassegna i progetti su una serie interessata; la precondizione dell'aggiornamento |
Applicata a semgrep/cve-2026-22732-demo su
Spring Security 6.4.12 vulnerabile, contro Tomcat embedded. Il suo HeaderVerificationTest asserisce
che gli header di sicurezza sono assenti, quindi una correzione funzionante lo fa fallire:
| Endpoint | Prima | Dopo |
|---|---|---|
/vuln/stream | X-Content-Type-Options: null | nosniff |
/vuln/content-length | X-Content-Type-Options: null | nosniff |
/safe | nosniff | nosniff |
X-Frame-Options e Cache-Control seguono lo stesso pattern.
Nei 16 repository del corpus che compilano (su 29 identificati), la correzione ha generato una configurazione per tre e ha correttamente lasciato invariati gli altri:
| Repository | Esito |
|---|---|
semgrep/cve-2026-22732-demo | Generata in com/example/vuln, accanto a @EnableWebSecurity; compila, header ripristinati |
nla/bamboo | Generata in ui/src/bamboo, accanto a @SpringBootApplication; compila. Il caso 7.0.3 gestito dal BOM che l'aggiornamento non può raggiungere |
star-whale/starwhale | Generata in ai/starwhale/mlops/configuration/security, accanto a @EnableWebSecurity; compila (JDK 11, il suo target dichiarato) |
hmcts/idam-web-public | Lasciato invariato — chiama già setShouldWriteHeadersEagerly |
okta/okta-idx-java (6.5.9), psi-probe (6.5.11), Ant-Media-Server (6.5.11) | Lasciati invariati — oltre la correzione sulla loro serie |
apache/shenyu (6.3.1) | Lasciato invariato — solo reattivo; HeaderWriterFilter non ha API servlet dietro, e la CVE è solo servlet |
brutusin/Brutusin-RPC (4.0.4) | Lasciato invariato — precede setShouldWriteHeadersEagerly (5.2) |
bootplus, template-app, front50, igor, rosco, spring-security, reportserver | Lasciati invariati — nessuna versione di Spring Security interessata risolta |
Rieseguire la correzione sui tre repository patchati non genera altro, quindi la rimedazione è idempotente rispetto alla propria output su progetti reali.
Un secondo corpus più ampio mira alla popolazione che la correzione affronta effettivamente — qualsiasi applicazione servlet Spring Security interessata, poiché non è richiesto alcun sink. Dei 64 progetti di questo tipo trovati tramite code search, 52 hanno compilato, 48 hanno risolto una versione interessata e 38 sono stati patchati; 35 di questi compilano (gli altri tre falliscono in modo identico senza il file generato). Tutti gli 8 progetti su Spring Security precedente alla 5.2 sono stati correttamente saltati. Vedi SUSCEPTIBLE-REPOSITORIES.md sezione 8.
OnCommittedResponseWrapper extends jakarta.servlet.http.HttpServletResponseWrapper, quindi
CVE-2026-22732 è solo servlet e un'applicazione reattiva non vi è esposta. I risultati di
FindHttpResponseContentLengthOrFlushBuffer segnalano il pattern reattivo analogo e richiedono comunque
una revisione manuale, ma la correzione deliberatamente non agisce su di essi. AddEagerHeaderWriterConfiguration
salta qualsiasi modulo che può vedere HeaderWriterFilter senza l'API servlet dietro — il
filter estende OncePerRequestFilter, e spring-security-web porta l'API servlet come
dipendenza provided non transitiva, quindi generare lì fallisce con
cannot access jakarta.servlet.Filter (osservato su apache/shenyu).HeaderWriterFilter.setShouldWriteHeadersEagerly arriva nella 5.2; sulla 4.0.4 il filter ha solo un
costruttore e doFilterInternal. La rilevazione segnala comunque le versioni EOL, ma la rimedazione è
trattenuta piuttosto che emettere una chiamata che non può compilare.| Tabella | Righe |
|---|---|
TaintFlowTable (da rewrite-program-analysis) | Una riga per ogni hit di taint dell'header Content-Length |
HttpResponseDirectCommitTable | Una riga per ogni hit strutturale WebFlux |
SpringSecurityVersionByProject | Una riga per ogni progetto con versione di Spring Security rilevata |
Tramite la CLI Moderne:
mod run . --recipe io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression
Tramite 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 elenca i 75 repository pubblici su cui queste ricette sono state sviluppate e misurate,
ancorati al commit esatto su cui ciascuno è stato valutato. Diversi sono mantenuti attivamente
e verranno patchati a monte, quindi la colonna changeset è ciò che rende i numeri seguenti
riproducibili piuttosto che meramente plausibili.
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
La sincronizzazione richiede circa 20 secondi e 1,4 GB; la build richiede circa 15 minuti ed è l'unico
passaggio lento. mod devcenter scrive devcenter.html in
corpus/.moderne/run/<runId>/, più uno per ogni sottodirectory di organizzazione.
La colonna org1 divide il corpus in gruppi che riflettono il motivo per cui ogni repository è presente
— Servlet Sinks e WebFlux Sinks per le due forme di chiamata vulnerabili, Patched per
i repository già rimediati a monte, Reference per Spring Security stesso e altri
non-consumatori, Verified per il caso verificato contro un server in esecuzione, Gradle per
la copertura degli strumenti di build, e Wide per il campione di massa.
Attenditi, sui commit ancorati:
| Risultato | |
|---|---|
| Scheda Upgrade | 39 Major, 21 Minor, 6 Patch, 4 Completed (70 repository) |
| Scheda Security | 65 repository esposti |
| Non applicabile | 5 repository non risolvono alcuna dipendenza Spring Security |
Quattro di quei cinque non usano davvero Spring Security — spring-projects/spring-security
è la libreria stessa, JoeyBling/bootplus usa Apache Shiro, jenkinsci/stapler punta
direttamente all'API servlet, e infofabrik/reportserver non ha alcuna build Maven o Gradle da
risolvere. Il quinto, xtuer/template-app, dichiara spring-security-web:5.0.0.RELEASE ma
la sua build Gradle non risolve alcuna dipendenza durante mod build, quindi nessuna ricetta può vedere
la versione. Trattalo come non misurato piuttosto che non interessato.
Per applicare la correzione e controllare il risultato:
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 scrive nei checkout in posizione. Un check completo sul corpus è lento
e farà emergere fallimenti non correlati a questa modifica — toolchain JDK mancanti, repository
di dipendenze irraggiungibili, test già rossi — quindi il segnale significativo è il
delta rispetto allo stesso comando eseguito prima dell'applicazione.
L'analisi del taint di rewrite-program-analysis gestisce il dataflow locale e i riepiloghi per-metodo, quindi questi pattern vengono rilevati automaticamente:
String h = "Content-Length"; response.setIntHeader(h, 42); viene segnalato — il framework traccia il taint del letterale attraverso l'assegnazione locale.Map, List, collezioni personalizzate). stash.put("k", "Content-Length") seguito da response.setIntHeader(stash.get("k"), 42) non viene rilevato — l'identità put/get è opaca per l'analisi.Moderne Proprietaria. Solo per uso da parte dei clienti Moderne secondo i termini di un contratto commerciale.