
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 권고에 따르면 영향 범위와 수정 버전은 다음과 같습니다:
| 시리즈 | 영향 범위 | 수정 버전 |
|---|---|---|
| 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) |
해당 시리즈의 수정 버전 이상(또는 7.0 / 6.5 이후의 향후 시리즈)의 Spring Security 버전을 해석하는 프로젝트는 영향받지 않는 것으로 처리되며 싱크 또는 파일별 마커를 받지 않습니다. 버전을 해석할 수 없는 프로젝트는 일반적인 패턴 기반 탐지로 폴백되어 스캐너가 반증할 수 없는 발견을 보고하는 쪽으로 오류를 범합니다. SpringSecurityVersionByProject 데이터 테이블은 여전히 해석된 버전을 기록하고 각 프로젝트를 영향 여부로 플래그하므로 필터링된 내용을 감사할 수 있습니다.
다음은 위험해 보이지만 래퍼가 추적하므로 응답이 커밋되기 전에 보안 헤더가 작성됩니다:
| 코드 | 안전한 이유 |
|---|---|
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)을 통해 라우팅됩니다. |
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.FindHttpResponseContentLengthHeader | setHeader / setIntHeader / addIntHeader(서블릿) 또는 HttpHeaders.set / add(WebFlux)에 도달하는 "Content-Length" 리터럴에 대한 오염 흐름 |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | WebFlux 무조건적 커밋: writeWith, writeAndFlushWith, setComplete, HttpHeaders.setContentLength |
다음을 실행하세요:
| 레시피 | 용도 |
|---|---|
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는 보안 헤더가 누락되었음을 주장하므로 작동하는 수정 사항은 이를 실패하게 만듭니다:
| 엔드포인트 | 수정 전 | 수정 후 |
|---|---|---|
/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 | @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 버전이 해석되지 않음 |
세 개의 패치된 저장소에 수정 사항을 다시 실행하면 추가 생성이 없으므로 수정은 실제 프로젝트에서 자체 출력에 대해 멱등적입니다.