
Рецепт OpenRewrite, который обнаруживает и исправляет подавление заголовков Spring Security (CVE-2026-22732), выявляя неправильное использование заголовка Content-Length и генерируя конфигурацию для немедленной записи заголовков.
OpenRewrite-рецепт, который обнаруживает код, подверженный CVE-2026-22732 — дефекту Spring Security, при котором установка Content-Length через один из трёх методов ответа обходит OnCommittedResponseWrapper в Spring Security. Поскольку обёртка никогда не видит заголовок, onResponseCommitted() не срабатывает, и лениво добавляемые заголовки безопасности (X-Frame-Options, X-Content-Type-Options, Cache-Control и т. д.) молча отбрасываются.
Фактические триггеры, подтверждённые на уязвимой версии Spring Security 6.4.12 с Spring Boot 3.4.3 / встроенным Tomcat:
Servlet Content-Length через перегрузки, обходящие обёртку
response.setHeader("Content-Length", "42");
response.setIntHeader("Content-Length", 42);
response.addIntHeader("Content-Length", 42);
Эти три перегрузки не переопределены в OnCommittedResponseWrapper. Последующие записи тела завершают объявленную длину, и контейнер фиксирует ответ без срабатывания ленивого записи заголовков.
WebFlux Content-Length через HttpHeaders
serverHttpResponse.getHeaders().set("Content-Length", "42");
serverHttpResponse.getHeaders().add("Content-Length", "42");
serverHttpResponse.getHeaders().setContentLength(42L);
Безусловные фиксации ответа в WebFlux
serverHttpResponse.writeWith(Mono.just(dataBuffer));
serverHttpResponse.writeAndFlushWith(publisher);
serverHttpResponse.setComplete();
Рецепт ограничен наличием Spring Security — он ничего не выдаёт в файлах, которые не ссылаются ни на один тип org.springframework.security.*, — а также затронутыми диапазонами версий Spring Security. Согласно рекомендации Spring, опубликованной 2026-03-19, затронутые диапазоны и исправленные версии:
| Серия | Затронутые | Исправленные |
|---|---|---|
| 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) |
Проекты, разрешающие версию Spring Security на уровне исправления или выше в своей серии (или в любой будущей серии после 7.0 / 6.5), считаются незатронутыми и не получают маркеров ни по отдельным точкам, ни по файлам. Проекты, где версию не удаётся разрешить, переходят к обычному обнаружению по шаблонам, чтобы сканер скорее сообщал о находке, которую не может опровергнуть. Таблица данных SpringSecurityVersionByProject по-прежнему записывает разрешённую версию и помечает каждый проект как затронутый или нет, чтобы вы могли проверить, что было отфильтровано.
Это выглядит опасно, но отслеживается обёрткой, поэтому заголовки безопасности записываются до фиксации ответа:
| Код | Почему это безопасно |
|---|---|
response.setContentLength(int) / setContentLengthLong(long) | Переопределено — обёртка записывает объявленную длину и вызывает onResponseCommitted(), когда тело завершается. |
response.flushBuffer() | Переопределено — вызывает doOnResponseCommitted() до super.flushBuffer(). |
response.getOutputStream().write(..) / flush() / close() | Возвращает SaveContextServletOutputStream; каждая запись/сброс/закрытие вызывает doOnResponseCommitted() перед делегированием. |
response.getWriter().write(..) / print(..) / println(..) / flush() / close() | Возвращает SaveContextPrintWriter; та же схема. |
response.addHeader("Content-Length", v) | Особый случай в обёртке — направляется через setContentLength(long). |
Конечная точка /vuln/flush в демо Semgrep утверждает, что триггером является flushBuffer(), но на уязвимой версии Spring Security 6.4.12 ответ фактически возвращает все шесть заголовков безопасности. Настоящие триггеры в демо — это вызовы setIntHeader("Content-Length", ...) в /vuln/stream и /vuln/content-length.
Запустите этот:
| Рецепт | Назначение |
|---|---|
io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression | Выполняет все проверки и формирует таблицу отчёта по версиям |
Агрегатор выше состоит из двух более мелких рецептов. Вы можете вызывать их по отдельности, если нужна только одна проверка.
| Рецепт | Назначение |
|---|---|
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthHeader | Поток заражения для литерала "Content-Length", достигающего setHeader / setIntHeader / addIntHeader (servlet) или HttpHeaders.set / add (WebFlux) |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | Безусловные фиксации в WebFlux: writeWith, writeAndFlushWith, setComplete, HttpHeaders.setContentLength |
Запустите этот:
| Рецепт | Назначение |
|---|---|
io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression | Выбирает самое дешёвое исправление, которое реально может применить каждый проект |
Он выполняет два шага по порядку.
1. Переход на исправление в собственной серии проекта. Spring Security опубликовал исправление как 6.5.9
и 7.0.4 в Maven Central. Каждый переход ограничен предусловием FindAffectedSpringSecuritySeries,
поскольку UpgradeDependencyVersion проверяет только, что целевая версия новее — при указании
7.0.4 он охотно перетащил бы проект на 5.8 через две мажорные версии.
2. Добавление конфигурации упреждающей записи заголовков для того, что шаг 1 не смог исправить. Это генерирует
один класс @Configuration на проект:
@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;
}
};
}
Запись заголовков заранее делает неважным, наблюдает ли обёртка за фиксацией, поэтому
это закрывает все точки в проекте сразу — включая те, до которых анализ заражения не может
добраться, например поток через Map в разделе «Известные ограничения». BeanPostProcessor видит фильтры, созданные
DSL HttpSecurity, потому что AutowireBeanFactoryObjectPostProcessor инициализирует их через
фабрику бинов. Две независимо опубликованные исправления для этой CVE используют ровно такую форму
(hmcts/idam-web-public, и
форки Spinnaker от armory-io через эквивалентный ObjectPostProcessor).
Шаг 2 покрывает проекты, которым шаг 1 не может помочь:
| Ситуация | Почему переход не работает |
|---|---|
| 5.7, 5.8, 6.3, 6.4 | Исправление поставляется только подписчикам Spring Enterprise — его нет в Maven Central |
| 6.0 - 6.2 | Для этих серий исправление никогда не выпускалось |
| Версия управляется импортированным BOM | Локально ничего не объявлено, что переход мог бы редактировать |
Последняя строка — не крайний случай. nla/bamboo разрешает 7.0.3 — серию с исправлением с открытым
исходным кодом — полностью из BOM Spring Boot, поэтому проверка версии сама по себе пропустила бы его на обоих шагах
и оставила бы уязвимым. Поэтому AddEagerHeaderWriterConfiguration откладывает переход только тогда,
когда у проекта есть доступное исправление с открытым исходным кодом и он объявляет собственную версию.
Сгенерированный класс размещается рядом с классом @EnableWebSecurity, если такой существует, с откатом
на @SpringBootApplication, а затем на любой @Configuration, так что он всегда попадает туда, куда
доходит компонентное сканирование. Проекты, которые уже записывают заголовки упреждающе или поставляют пропатченный
OnCommittedResponseWrapper (как jogetworkflow/jw-community),
не трогаются.
| Рецепт | Назначение |
|---|---|
io.moderne.recipe.cve202622732.UpgradeSpringSecurityToPatchedVersion | Переход с ограничением по серии на 6.5.9 / 7.0.4 |
io.moderne.recipe.cve202622732.AddEagerHeaderWriterConfiguration | Сгенерированная конфигурация, отдельно |
io.moderne.recipe.cve202622732.FindAffectedSpringSecuritySeries | Помечает проекты на затронутой серии; предусловие перехода |
Применено к semgrep/cve-2026-22732-demo на
уязвимой версии Spring Security 6.4.12, против встроенного Tomcat. Его HeaderVerificationTest утверждает,
что заголовки безопасности отсутствуют, поэтому рабочее исправление заставляет его падать:
| Конечная точка | До | После |
|---|---|---|
/vuln/stream | X-Content-Type-Options: null | nosniff |
/vuln/content-length | X-Content-Type-Options: null | nosniff |
/safe | nosniff | nosniff |
X-Frame-Options и Cache-Control следуют той же схеме.
По 16 репозиториям корпуса, которые собираются (из 29 выявленных), исправление сгенерировало конфигурацию для трёх и корректно оставило остальные без изменений:
| Репозиторий | Результат |
|---|---|
semgrep/cve-2026-22732-demo | Сгенерировано в com/example/vuln, рядом с @EnableWebSecurity; компилируется, заголовки восстановлены |
nla/bamboo | Сгенерировано в ui/src/bamboo, рядом с @SpringBootApplication; компилируется. Случай с управляемой через BOM версией 7.0.3, до которого переход не может добраться |
star-whale/starwhale | Сгенерировано в ai/starwhale/mlops/configuration/security, рядом с @EnableWebSecurity; компилируется (JDK 11, его объявленная цель) |
hmcts/idam-web-public | Оставлен без изменений — уже вызывает setShouldWriteHeadersEagerly |
okta/okta-idx-java (6.5.9), psi-probe (6.5.11), Ant-Media-Server (6.5.11) | Оставлены без изменений — уже после исправления в своей серии |
apache/shenyu (6.3.1) | Оставлен без изменений — только реактивный; за HeaderWriterFilter нет servlet API, а CVE касается только servlet |
brutusin/Brutusin-RPC (4.0.4) | Оставлен без изменений — предшествует setShouldWriteHeadersEagerly (5.2) |
bootplus, template-app, front50, igor, rosco, spring-security, reportserver | Оставлены без изменений — не разрешена затронутая версия Spring Security |
Повторный запуск исправления по трём пропатченным репозиториям ничего дополнительно не генерирует, поэтому исправление идемпотентно относительно собственного вывода на реальных проектах.
Второй, более широкий корпус нацелен на популяцию, которую исправление фактически охватывает — любое затронутое servlet-приложение Spring Security, поскольку точка не требуется. Из 64 таких проектов, найденных поиском по коду, 52 собрались, 48 разрешили затронутую версию, и 38 были пропатчены; 35 из них компилируются (остальные три падают одинаково и без сгенерированного файла). Все 8 проектов на Spring Security ниже 5.2 были корректно пропущены. См. раздел 8 в SUSCEPTIBLE-REPOSITORIES.md.
OnCommittedResponseWrapper extends jakarta.servlet.http.HttpServletResponseWrapper, поэтому
CVE-2026-22732 касается только servlet, и реактивное приложение ей не подвержено. Находки из
FindHttpResponseContentLengthOrFlushBuffer помечают аналогичный реактивный паттерн и всё ещё требуют
ручной проверки, но исправление намеренно на них не действует. AddEagerHeaderWriterConfiguration
пропускает любой модуль, который видит HeaderWriterFilter без servlet API за ним —
фильтр расширяет OncePerRequestFilter, а spring-security-web несёт servlet API как
нетранзитивную зависимость provided, поэтому генерация там падает с
cannot access jakarta.servlet.Filter (наблюдалось на apache/shenyu).HeaderWriterFilter.setShouldWriteHeadersEagerly появляется в 5.2; на 4.0.4 у фильтра есть только
конструктор и doFilterInternal. Обнаружение по-прежнему сообщает об EOL-версиях, но исправление
удерживается, а не выдаёт вызов, который не может скомпилироваться.| Таблица | Строки |
|---|---|
TaintFlowTable (из rewrite-program-analysis) | По одной строке на каждое попадание заражения заголовка Content-Length |
HttpResponseDirectCommitTable | По одной строке на каждое структурное попадание WebFlux |
SpringSecurityVersionByProject | По одной строке на проект с обнаруженной версией Spring Security |
Через Moderne CLI:
mod run . --recipe io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression
Через 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 перечисляет 75 публичных репозиториев, на которых эти рецепты разрабатывались и измерялись,
закреплённых за точным коммитом, на котором каждый оценивался. Некоторые активно поддерживаются
и будут пропатчены выше по течению, поэтому колонка changeset — это то, что делает числа ниже
воспроизводимыми, а не просто правдоподобными.
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
Синхронизация занимает около 20 секунд и 1,4 ГБ; сборка занимает примерно 15 минут и является
единственным медленным шагом. mod devcenter записывает devcenter.html в
corpus/.moderne/run/<runId>/, плюс по одному на подкаталог организации.
Колонка org1 делит корпус на группы, отражающие причину присутствия каждого репозитория
— Servlet Sinks и WebFlux Sinks для двух уязвимых форм вызовов, Patched для
репозиториев, уже исправленных выше по течению, Reference для самого Spring Security и других
не-потребителей, Verified для случая, проверенного на работающем сервере, Gradle для
покрытия инструментов сборки и Wide для массовой выборки.
Ожидайте на закреплённых коммитах:
| Результат | |
|---|---|
| Карточка обновления | 39 Major, 21 Minor, 6 Patch, 4 Completed (70 репозиториев) |
| Карточка безопасности | 65 подверженных репозиториев |
| Не применимо | 5 репозиториев не разрешают ни одной зависимости Spring Security |
Четыре из этих пяти действительно не используют Spring Security — spring-projects/spring-security
это сама библиотека, JoeyBling/bootplus использует Apache Shiro, jenkinsci/stapler нацелен
непосредственно на servlet API, а infofabrik/reportserver не имеет сборки Maven или Gradle для
разрешения. Пятый, xtuer/template-app, объявляет spring-security-web:5.0.0.RELEASE, но
его сборка Gradle вообще не разрешает зависимостей во время mod build, поэтому ни один рецепт не может увидеть
версию. Считайте его неизмеренным, а не незатронутым.
Чтобы применить исправление и проверить результат:
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 записывает в рабочие копии на месте. Полная check по корпусу медленная
и выявит сбои, не связанные с этим изменением — отсутствующие цепочки инструментов JDK, недоступные
репозитории зависимостей, тесты, которые уже были красными, — поэтому значимым сигналом является
дельта относительно той же команды, запущенной до применения.
Анализ заражения из rewrite-program-analysis обрабатывает локальный поток данных и сводки по методам, поэтому эти паттерны обнаруживаются автоматически:
String h = "Content-Length"; response.setIntHeader(h, 42); помечается — фреймворк отслеживает заражение литерала через локальное присваивание.Map, List, пользовательские коллекции). stash.put("k", "Content-Length") с последующим response.setIntHeader(stash.get("k"), 42) не обнаруживается — тождественность put/get непрозрачна для анализа.Moderne Proprietary. Только для использования клиентами Moderne на условиях коммерческого контракта.