
Recette OpenRewrite qui détecte et corrige la suppression d'en-têtes Spring Security (CVE-2026-22732) en identifiant l'utilisation abusive de l'en-tête Content-Length et en générant une configuration d'écriture anticipée des en-têtes.
Recette OpenRewrite qui détecte le code susceptible d'être vulnérable à CVE-2026-22732, un défaut de Spring Security où la définition de Content-Length via l'une des trois méthodes de réponse contourne OnCommittedResponseWrapper de Spring Security. Comme le wrapper ne voit jamais l'en-tête, onResponseCommitted() ne se déclenche jamais, et les en-têtes de sécurité ajoutés paresseusement (X-Frame-Options, X-Content-Type-Options, Cache-Control, etc.) sont silencieusement supprimés.
Les déclencheurs réels, confirmés contre Spring Security 6.4.12 vulnérable avec Spring Boot 3.4.3 / Tomcat embarqué :
Content-Length servlet via les surcharges contournant le wrapper
response.setHeader("Content-Length", "42");
response.setIntHeader("Content-Length", 42);
response.addIntHeader("Content-Length", 42);
Ces trois surcharges ne sont pas redéfinies dans OnCommittedResponseWrapper. Les écritures ultérieures du corps complètent la longueur déclarée et le conteneur valide la réponse sans déclencher l'écriture paresseuse des en-têtes.
Content-Length WebFlux via HttpHeaders
serverHttpResponse.getHeaders().set("Content-Length", "42");
serverHttpResponse.getHeaders().add("Content-Length", "42");
serverHttpResponse.getHeaders().setContentLength(42L);
Validations de réponse WebFlux inconditionnelles
serverHttpResponse.writeWith(Mono.just(dataBuffer));
serverHttpResponse.writeAndFlushWith(publisher);
serverHttpResponse.setComplete();
La recette est conditionnée à la présence de Spring Security — elle n'émet rien dans les fichiers qui ne référencent aucun type org.springframework.security.* — et aux plages de versions Spring Security affectées. Selon l'avis Spring publié le 2026-03-19, les plages affectées et les versions corrigées sont :
| Série | Affectée | Corrigée |
|---|---|---|
| 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) |
Les projets résolvant une version de Spring Security égale ou supérieure au correctif de leur série (ou sur toute série future au-delà de 7.0 / 6.5) sont considérés comme non affectés et ne reçoivent aucun marqueur par sink ou par fichier. Les projets dont la version ne peut pas être résolue retombent sur la détection habituelle basée sur les motifs, afin qu'un scanner privilégie le signalement d'un résultat qu'il ne peut pas réfuter. La table de données SpringSecurityVersionByProject enregistre toujours la version résolue et marque chaque projet comme affecté ou non, ce qui permet d'auditer ce qui a été filtré.
Ces éléments semblent dangereux mais sont suivis par le wrapper, donc les en-têtes de sécurité sont écrits avant la validation de la réponse :
| Code | Pourquoi c'est sûr |
|---|---|
response.setContentLength(int) / setContentLengthLong(long) | Redéfini — le wrapper enregistre la longueur déclarée et déclenche onResponseCommitted() lorsque le corps est terminé. |
response.flushBuffer() | Redéfini — appelle doOnResponseCommitted() avant super.flushBuffer(). |
response.getOutputStream().write(..) / flush() / close() | Renvoie SaveContextServletOutputStream ; chaque write/flush/close déclenche doOnResponseCommitted() avant de déléguer. |
response.getWriter().write(..) / print(..) / println(..) / flush() / close() | Renvoie SaveContextPrintWriter ; même schéma. |
response.addHeader("Content-Length", v) | Cas particulier dans le wrapper — routé via setContentLength(long). |
Le point de terminaison /vuln/flush de la démo Semgrep prétend que flushBuffer() est le déclencheur, mais sur un Spring Security 6.4.12 vulnérable, la réponse renvoie en réalité les six en-têtes de sécurité. Les véritables déclencheurs de la démo sont les appels setIntHeader("Content-Length", ...) dans /vuln/stream et /vuln/content-length.
Exécutez celle-ci :
| Recette | Objectif |
|---|---|
io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression | Exécute toutes les détections et émet le rapport de versions |
L'agrégateur ci-dessus est composé de deux recettes plus petites. Vous pouvez les invoquer individuellement si vous ne souhaitez qu'une seule détection.
| Recette | Objectif |
|---|---|
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthHeader | Flux de taint pour le littéral "Content-Length" atteignant setHeader / setIntHeader / addIntHeader (servlet) ou HttpHeaders.set / add (WebFlux) |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | Validations WebFlux inconditionnelles : writeWith, writeAndFlushWith, setComplete, HttpHeaders.setContentLength |
Exécutez celle-ci :
| Recette | Objectif |
|---|---|
io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression | Choisit la remédiation la moins coûteuse que chaque projet peut réellement appliquer |
Elle exécute deux étapes dans l'ordre.
1. Mise à niveau vers le correctif de la propre série du projet. Spring Security a publié le correctif en versions 6.5.9 et 7.0.4 sur Maven Central. Chaque mise à niveau est conditionnée par une précondition FindAffectedSpringSecuritySeries, car UpgradeDependencyVersion vérifie uniquement que sa cible est plus récente — si on lui demandait d'aller vers 7.0.4, elle ferait passer un projet 5.8 à travers deux versions majeures sans sourciller.
2. Ajout d'une configuration d'écriture anticipée des en-têtes pour tout ce que l'étape 1 n'a pas pu corriger. Cela génère une classe @Configuration par projet :
@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;
}
};
}
Écrire les en-têtes en amont rend sans importance le fait que le wrapper observe ou non la validation, ce qui ferme tous les sinks du projet d'un coup — y compris ceux que l'analyse de taint ne peut pas atteindre, comme le flux Map sous « Limitations connues ». Le BeanPostProcessor voit les filtres construits par le DSL HttpSecurity car AutowireBeanFactoryObjectPostProcessor les initialise via la fabrique de beans. Deux correctifs publiés indépendamment pour cette CVE utilisent exactement cette forme (hmcts/idam-web-public, et les forks Spinnaker d'armory-io via l'ObjectPostProcessor équivalent).
L'étape 2 couvre les projets que l'étape 1 ne peut pas aider :
| Situation | Pourquoi la mise à niveau ne fonctionne pas |
|---|---|
| 5.7, 5.8, 6.3, 6.4 | Le correctif est réservé aux abonnés Spring Enterprise — pas sur Maven Central |
| 6.0 - 6.2 | Aucun correctif n'a jamais été publié sur ces séries |
| Version gérée par un BOM importé | Rien n'est déclaré localement que la mise à niveau puisse modifier |
Cette dernière ligne n'est pas un cas marginal. nla/bamboo résout 7.0.3 — une série avec un correctif open-source — entièrement depuis le BOM Spring Boot, donc une simple vérification de version l'ignorerait sous les deux étapes et le laisserait vulnérable. AddEagerHeaderWriterConfiguration ne diffère donc vers la mise à niveau que lorsque le projet dispose à la fois d'un correctif open-source disponible et déclare une version qui lui est propre.
La classe générée est placée à côté d'une classe @EnableWebSecurity lorsqu'elle existe, avec repli sur @SpringBootApplication puis sur toute @Configuration, afin qu'elle atterrisse toujours quelque part où le scan de composants peut l'atteindre. Les projets qui écrivent déjà les en-têtes de manière anticipée, ou qui intègrent un OnCommittedResponseWrapper corrigé (comme jogetworkflow/jw-community), sont laissés tranquilles.
| Recette | Objectif |
|---|---|
io.moderne.recipe.cve202622732.UpgradeSpringSecurityToPatchedVersion | Mise à niveau conditionnée par série vers 6.5.9 / 7.0.4 |
io.moderne.recipe.cve202622732.AddEagerHeaderWriterConfiguration | La configuration générée, seule |
io.moderne.recipe.cve202622732.FindAffectedSpringSecuritySeries | Marque les projets sur une série affectée ; la précondition de mise à niveau |
Appliqué à semgrep/cve-2026-22732-demo sur Spring Security 6.4.12 vulnérable, contre Tomcat embarqué. Son HeaderVerificationTest affirme que les en-têtes de sécurité sont absents, donc un correctif fonctionnel le fait échouer :
| Point de terminaison | Avant | Après |
|---|---|---|
/vuln/stream | X-Content-Type-Options: null | nosniff |
/vuln/content-length | X-Content-Type-Options: null | nosniff |
/safe | nosniff | nosniff |
X-Frame-Options et Cache-Control suivent le même schéma.
Sur les 16 dépôts du corpus qui compilent (sur 29 identifiés), le correctif a généré une configuration pour trois d'entre eux et a correctement laissé les autres tranquilles :
| Dépôt | Résultat |
|---|---|
semgrep/cve-2026-22732-demo | Généré dans com/example/vuln, à côté de @EnableWebSecurity ; compile, en-têtes restaurés |
nla/bamboo | Généré dans ui/src/bamboo, à côté de @SpringBootApplication ; compile. Le cas 7.0.3 géré par BOM que la mise à niveau ne peut pas atteindre |
star-whale/starwhale | Généré dans ai/starwhale/mlops/configuration/security, à côté de @EnableWebSecurity ; compile (JDK 11, sa cible déclarée) |
hmcts/idam-web-public | Laissé tranquille — appelle déjà setShouldWriteHeadersEagerly |
okta/okta-idx-java (6.5.9), psi-probe (6.5.11), Ant-Media-Server (6.5.11) | Laissés tranquilles — au-delà du correctif de leur série |
apache/shenyu (6.3.1) | Laissé tranquille — réactif uniquement ; HeaderWriterFilter n'a aucune API servlet derrière lui, et la CVE concerne uniquement le servlet |
brutusin/Brutusin-RPC (4.0.4) | Laissé tranquille — antérieur à setShouldWriteHeadersEagerly (5.2) |
bootplus, template-app, front50, igor, rosco, spring-security, reportserver | Laissés tranquilles — aucune version Spring Security affectée résolue |
Ré-exécuter le correctif sur les trois dépôts corrigés ne génère rien de plus, donc la remédiation est idempotente vis-à-vis de sa propre sortie sur des projets réels.
Un second corpus plus large cible la population que le correctif adresse réellement — toute application servlet Spring Security affectée, car aucun sink n'est requis. Sur 64 projets de ce type trouvés par recherche de code, 52 ont compilé, 48 ont résolu une version affectée, et 38 ont été corrigés ; 35 d'entre eux compilent (les trois autres échouent de manière identique sans le fichier généré). Les 8 projets sur Spring Security inférieur à 5.2 ont été correctement ignorés. Voir la section 8 de SUSCEPTIBLE-REPOSITORIES.md.
OnCommittedResponseWrapper extends jakarta.servlet.http.HttpServletResponseWrapper, donc
CVE-2026-22732 concerne uniquement le servlet et une application réactive n'y est pas exposée. Les résultats de
FindHttpResponseContentLengthOrFlushBuffer signalent le schéma réactif analogue et nécessitent toujours une
revue manuelle, mais le correctif n'agit délibérément pas sur eux. AddEagerHeaderWriterConfiguration
ignore tout module qui peut voir HeaderWriterFilter sans l'API servlet derrière lui — le
filtre étend OncePerRequestFilter, et spring-security-web porte l'API servlet comme dépendance
provided non transitive, donc générer là échoue avec
cannot access jakarta.servlet.Filter (observé sur apache/shenyu).HeaderWriterFilter.setShouldWriteHeadersEagerly arrive en 5.2 ; sur 4.0.4, le filtre n'a qu'un
constructeur et doFilterInternal. La détection signale toujours les versions EOL, mais la remédiation est
retenue plutôt que d'émettre un appel qui ne peut pas compiler.| Table | Lignes |
|---|---|
TaintFlowTable (de rewrite-program-analysis) | Une ligne par hit de taint d'en-tête Content-Length |
HttpResponseDirectCommitTable | Une ligne par hit structurel WebFlux |
SpringSecurityVersionByProject | Une ligne par projet avec version Spring Security détectée |
Via la CLI Moderne :
mod run . --recipe io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression
Via 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 liste les 75 dépôts publics sur lesquels ces recettes ont été développées et mesurées,
épinglés au commit exact auquel chacun a été évalué. Plusieurs sont activement maintenus
et seront corrigés en amont, donc la colonne changeset est ce qui rend les chiffres ci-dessous
reproductibles plutôt que simplement plausibles.
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 synchronisation prend environ 20 secondes et 1,4 Go ; la compilation prend environ 15 minutes et est la
seule étape lente. mod devcenter écrit devcenter.html dans
corpus/.moderne/run/<runId>/, plus un par sous-répertoire d'organisation.
La colonne org1 divise le corpus en groupes qui reflètent pourquoi chaque dépôt est présent
— Servlet Sinks et WebFlux Sinks pour les deux formes d'appel vulnérables, Patched pour
les dépôts déjà remédiés en amont, Reference pour Spring Security lui-même et les autres
non-consommateurs, Verified pour le cas vérifié contre un serveur en cours d'exécution, Gradle pour
la couverture des outils de compilation, et Wide pour l'échantillon de masse.
Attendez-vous, sur les commits épinglés :
| Résultat | |
|---|---|
| Carte de mise à niveau | 39 Majeures, 21 Mineures, 6 Correctifs, 4 Terminées (70 dépôts) |
| Carte de sécurité | 65 dépôts exposés |
| Non applicable | 5 dépôts ne résolvent aucune dépendance Spring Security |
Quatre de ces cinq n'utilisent réellement pas Spring Security — spring-projects/spring-security
est la bibliothèque elle-même, JoeyBling/bootplus utilise Apache Shiro, jenkinsci/stapler cible
directement l'API servlet, et infofabrik/reportserver n'a aucune compilation Maven ou Gradle à
résoudre. Le cinquième, xtuer/template-app, déclare spring-security-web:5.0.0.RELEASE mais
sa compilation Gradle ne résout aucune dépendance pendant mod build, donc aucune recette ne peut voir
la version. Traitez-le comme non mesuré plutôt que non affecté.
Pour appliquer le correctif et vérifier le résultat :
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 écrit dans les checkouts en place. Un check complet sur le corpus est lent
et fera remonter des échecs sans rapport avec ce changement — toolchains JDK manquantes, dépôts
de dépendances inaccessibles, tests déjà rouges — donc le signal pertinent est le
delta par rapport à la même commande exécutée avant l'application.
L'analyse de taint de rewrite-program-analysis gère le flux de données local et les résumés par méthode, donc ces schémas sont détectés automatiquement :
String h = "Content-Length"; response.setIntHeader(h, 42); est signalé — le framework suit le taint du littéral à travers l'affectation locale.Map, List, collections personnalisées). stash.put("k", "Content-Length") suivi de response.setIntHeader(stash.get("k"), 42) n'est pas détecté — l'identité put/get est opaque pour l'analyse.Moderne Propriétaire. Réservé à l'usage des clients Moderne selon les termes d'un contrat commercial.