
OpenRewrite 레시피로, Content-Length 헤더 오용을 식별하고 즉시 헤더 작성 구성을 생성하여 Spring Security 헤더 억제(CVE-2026-22732)를 탐지하고 수정합니다.
CVE-2026-22732에 취약한 코드를 탐지하는 OpenRewrite 레시피입니다. 이는 세 가지 응답 메서드 중 하나를 통해 Content-Length를 설정하면 Spring Security의 OnCommittedResponseWrapper를 우회하는 Spring Security 결함입니다. 래퍼가 헤더를 인식하지 못하므로 onResponseCommitted()가 실행되지 않고, 지연 추가되는 보안 헤더(X-Frame-Options, X-Content-Type-Options, Cache-Control 등)가 조용히 누락됩니다.
취약한 Spring Security 6.4.12 + Spring Boot 3.4.3 / 내장 Tomcat에서 확인된 실제 트리거는 다음과 같습니다:
래퍼를 우회하는 오버로드를 통한 서블릿 Content-Length
response.setHeader("Content-Length", "42");
response.setIntHeader("Content-Length", 42);
response.addIntHeader("Content-Length", 42);
이 세 가지 오버로드는 OnCommittedResponseWrapper에서 재정의되지 않습니다. 이후 본문 쓰기가 선언된 길이를 완성하고 컨테이너는 지연 헤더 작성기를 실행하지 않고 커밋합니다.
HttpHeaders를 통한 WebFlux Content-Length
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 버전 범위에 따라 게이팅됩니다. 2026-03-19에 게시된 Spring 권고에 따르면 영향 범위와 수정 버전은 다음과 같습니다:
해당 시리즈의 수정 버전 이상(또는 7.0 / 6.5 이후의 향후 시리즈)의 Spring Security 버전을 해석하는 프로젝트는 영향받지 않는 것으로 처리되며 싱크 또는 파일별 마커를 받지 않습니다. 버전을 해석할 수 없는 프로젝트는 일반적인 패턴 기반 탐지로 폴백되어 스캐너가 반증할 수 없는 발견을 보고하는 쪽으로 오류를 범합니다. SpringSecurityVersionByProject 데이터 테이블은 여전히 해석된 버전을 기록하고 각 프로젝트를 영향 여부로 플래그하므로 필터링된 내용을 감사할 수 있습니다.
다음은 위험해 보이지만 래퍼가 추적하므로 응답이 커밋되기 전에 보안 헤더가 작성됩니다:
Semgrep 데모의 /vuln/flush 엔드포인트는 flushBuffer()가 트리거라고 주장하지만, 취약한 Spring Security 6.4.12에서 응답은 실제로 여섯 개의 보안 헤더를 모두 반환합니다. 데모의 실제 트리거는 /vuln/stream 및 /vuln/content-length의 setIntHeader("Content-Length", ...) 호출입니다.
다음을 실행하세요:
| 레시피 | 용도 |
|---|---|
io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression | 모든 탐지를 실행하고 버전 보고 테이블을 출력합니다 |
위의 집계기는 두 개의 더 작은 레시피로 구성됩니다. 하나의 탐지만 원하는 경우 개별적으로 호출할 수 있습니다.
다음을 실행하세요:
| 레시피 | 용도 |
|---|---|
io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression | 각 프로젝트가 실제로 취할 수 있는 가장 저렴한 수정을 선택합니다 |
두 단계를 순서대로 실행합니다.
1. 프로젝트 자체 시리즈의 수정 버전으로 업그레이드. Spring Security는 수정 버전을 Maven Central에 6.5.9 및 7.0.4로 게시했습니다. 각 업그레이드는 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는 AutowireBeanFactoryObjectPostProcessor가 빈 팩토리를 통해 필터를 초기화하므로 HttpSecurity DSL로 빌드된 필터를 볼 수 있습니다. 이 CVE에 대해 독립적으로 게시된 두 가지 수정 사항이 정확히 이 형태를 사용합니다(hmcts/idam-web-public 및 armory-io의 Spinnaker 포크는 동등한 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을 전적으로 Spring Boot BOM에서 해석하므로 버전 확인만으로는 두 단계 모두에서 건너뛰고 취약한 상태로 남게 됩니다. 따라서 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 | 영향받는 시리즈의 프로젝트를 표시합니다. 업그레이드 사전 조건입니다 |
취약한 Spring Security 6.4.12의 semgrep/cve-2026-22732-demo에 내장 Tomcat을 대상으로 적용했습니다. 해당 HeaderVerificationTest는 보안 헤더가 누락되었음을 주장하므로 작동하는 수정 사항은 이를 실패하게 만듭니다:
X-Frame-Options 및 Cache-Control도 동일한 패턴을 따릅니다.
빌드되는 코퍼스의 16개 저장소(식별된 29개 중)에서 수정 사항은 세 개에 대한 구성을 생성하고 나머지는 올바르게 그대로 두었습니다:
세 개의 패치된 저장소에 수정 사항을 다시 실행하면 추가 생성이 없으므로 수정은 실제 프로젝트에서 자체 출력에 대해 멱등적입니다.
두 번째 더 넓은 코퍼스는 수정 사항이 실제로 다루는 모집단(싱크가 필요 없으므로 영향받는 모든 서블릿 Spring Security 애플리케이션)을 대상으로 합니다. 코드 검색으로 찾은 64개 프로젝트 중 52개가 빌드되었고, 48개가 영향받는 버전을 해석했으며, 38개가 패치되었고 그중 35개가 컴파일됩니다(나머지 3개는 생성된 파일 없이도 동일하게 실패). Spring Security 5.2 미만의 8개 프로젝트는 모두 올바르게 건너뛰었습니다. SUSCEPTIBLE-REPOSITORIES.md 섹션 8을 참조하세요.
OnCommittedResponseWrapper extends jakarta.servlet.http.HttpServletResponseWrapper이므로 CVE-2026-22732는 서블릿 전용이며 리액티브 애플리케이션은 이에 노출되지 않습니다. FindHttpResponseContentLengthOrFlushBuffer의 발견은 유사한 리액티브 패턴을 플래그하며 여전히 수동 검토가 필요하지만 수정 사항은 의도적으로 이에 대해 조치하지 않습니다. AddEagerHeaderWriterConfiguration은 뒤에 서블릿 API 없이 HeaderWriterFilter를 볼 수 있는 모든 모듈을 건너뜁니다. 필터는 OncePerRequestFilter를 확장하고 spring-security-web은 서블릿 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.4GB가 걸리고, 빌드는 약 15분이 걸리며 유일하게 느린 단계입니다. mod devcenter는 corpus/.moderne/run/<runId>/에 devcenter.html을 작성하고 조직 하위 디렉터리당 하나씩 추가로 작성합니다.
org1 열은 각 저장소가 존재하는 이유를 반영하는 그룹으로 코퍼스를 분할합니다. 두 가지 취약한 호출 형태에 대한 Servlet Sinks 및 WebFlux Sinks, 이미 업스트림에서 수정된 저장소에 대한 Patched, Spring Security 자체 및 기타 비소비자에 대한 Reference, 실행 중인 서버에 대해 확인된 사례에 대한 Verified, 빌드 도구 범위에 대한 Gradle, 대량 샘플에 대한 Wide입니다.
고정된 커밋에서 기대할 수 있는 결과:
| 결과 | |
|---|---|
| 업그레이드 카드 | 메이저 39, 마이너 21, 패치 6, 완료 4 (저장소 70개) |
| 보안 카드 | 노출된 저장소 65개 |
| 해당 없음 | Spring Security 종속성을 해석하지 않는 저장소 5개 |
그 다섯 개 중 네 개는 실제로 Spring Security를 사용하지 않습니다. spring-projects/spring-security는 라이브러리 자체이고, JoeyBling/bootplus는 Apache Shiro를 사용하며, jenkinsci/stapler는 서블릿 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 독점. 상업 계약 조건에 따라 Moderne 고객만 사용할 수 있습니다.
| 시리즈 | 영향 범위 | 수정 버전 |
|---|
| 5.7.x | 5.7.0 – 5.7.21 | 5.7.22 (엔터프라이즈) |
| 5.8.x | 5.8.0 – 5.8.23 | 5.8.24 (엔터프라이즈) |
| 6.3.x | 6.3.0 – 6.3.14 | 6.3.15 (엔터프라이즈) |
| 6.4.x | 6.4.0 – 6.4.14 | 6.4.15 (엔터프라이즈) |
| 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) |
| 코드 | 안전한 이유 |
|---|
response.setContentLength(int) / setContentLengthLong(long) | 재정의됨 — 래퍼가 선언된 길이를 기록하고 본문이 완료되면 onResponseCommitted()를 실행합니다. |
response.flushBuffer() | 재정의됨 — super.flushBuffer() 이전에 doOnResponseCommitted()를 호출합니다. |
response.getOutputStream().write(..) / flush() / close() | SaveContextServletOutputStream을 반환합니다. 모든 쓰기/플러시/닫기는 위임 전에 doOnResponseCommitted()를 실행합니다. |
response.getWriter().write(..) / print(..) / println(..) / flush() / close() | SaveContextPrintWriter를 반환합니다. 동일한 패턴입니다. |
response.addHeader("Content-Length", v) | 래퍼에서 특수 처리됨 — setContentLength(long)을 통해 라우팅됩니다. |
| 레시피 | 용도 |
|---|
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthHeader | setHeader / setIntHeader / addIntHeader(서블릿) 또는 HttpHeaders.set / add(WebFlux)에 도달하는 "Content-Length" 리터럴에 대한 오염 흐름 |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | WebFlux 무조건적 커밋: writeWith, writeAndFlushWith, setComplete, HttpHeaders.setContentLength |
| 엔드포인트 | 수정 전 | 수정 후 |
|---|
/vuln/stream | X-Content-Type-Options: null | nosniff |
/vuln/content-length | X-Content-Type-Options: null | nosniff |
/safe | nosniff | nosniff |
| 저장소 | 결과 |
|---|
semgrep/cve-2026-22732-demo | @EnableWebSecurity 옆의 com/example/vuln에 생성됨. 컴파일되고 헤더가 복원됨 |
nla/bamboo | @SpringBootApplication 옆의 ui/src/bamboo에 생성됨. 컴파일됨. 업그레이드가 도달할 수 없는 BOM 관리 7.0.3 사례 |
star-whale/starwhale | @EnableWebSecurity 옆의 ai/starwhale/mlops/configuration/security에 생성됨. 컴파일됨(선언된 대상인 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 뒤에 서블릿 API가 없고 CVE는 서블릿 전용 |
brutusin/Brutusin-RPC (4.0.4) | 그대로 둠 — setShouldWriteHeadersEagerly(5.2) 이전 |
bootplus, template-app, front50, igor, rosco, spring-security, reportserver | 그대로 둠 — 영향받는 Spring Security 버전이 해석되지 않음 |