Spring Security가 설정한 보안 헤더를 조용히 삼켜버리는 CVE-2026-22732가 3월에 공개됐는데, 영향 범위가 5.7부터 7.0까지 사실상 운영 중인 거의 모든 버전이다.
금요일 새벽 2시, 슬랙에 보안팀 알림이 떴다.
🚨 주간 보안 스캔 결과: CSP 헤더 미탐지, HSTS 미탐지, X-Frame-Options 미탐지
처음엔 스캐너 오탐이라고 생각했다. SecurityFilterChain에 설정 다 해놨으니까. .headers(h -> h.contentSecurityPolicy(...).httpStrictTransportSecurity(...)) — 코드 리뷰도 통과했고, 스테이징에서도 확인했던 건데.
curl로 직접 찍어봤다.
curl -I https://api.our-service.com/health
진짜 없다. X-Content-Type-Options도, X-Frame-Options도, 다 사라져 있었다.
범인은 3월 19일에 공개된 CVE-2026-22732였다. CVSS 9.1 크리티컬.
스프링의 보안 모듈이 서블릿 환경에서 HTTP 응답 헤더를 설정할 때, 특정 조건에서 쓰기 자체가 실행되지 않는 버그다. 설정은 멀쩡하고, 에러 로그도 없고, 앱은 정상 동작한다. 그냥 보안 관련 응답값만 조용히 빠진다.
이게 무서운 이유: 아무도 모른다. 기능 테스트 다 통과한다. 200 OK 잘 내려온다. 보안 스캔을 주기적으로 안 돌리고 있었으면 몇 달째 방어 설정 없이 서비스하고 있었을 거다.
영향 범위가 미친 수준이다
영향받는 버전 목록을 보고 한숨이 나왔다.
Spring Security 5.7.0 ~ 5.7.21
Spring Security 5.8.0 ~ 5.8.23
Spring Security 6.3.0 ~ 6.3.14
Spring Security 6.4.0 ~ 6.4.14
Spring Security 6.5.0 ~ 6.5.8
Spring Security 7.0.0 ~ 7.0.3
사실상 현재 운영 중인 거의 모든 버전이 걸린다. 5.7부터 7.0까지, 지원 중인 브랜치 전부.
응답에서 보호 설정이 빠지면 뭐가 열리냐면:
CSP 없음 → XSS 공격에 브라우저 레벨 방어선 증발
HSTS 없음 → MITM으로 HTTP 다운그레이드 가능
X-Frame-Options 없음 → 클릭재킹에 노출
Cache-Control 없음 → 프록시/CDN이 민감 데이터를 캐싱
인증도 필요 없고, 사용자 인터랙션도 필요 없다. 그냥 요청 보내면 보호 안 된 응답이 온다.
새벽 대응 — 두 갈래 전략
패치 버전이 나와 있었지만 새벽에 해당 프레임워크 버전 올리고 배포하는 건 부담이 컸다. 그래서 두 단계로 나눴다.
즉시 조치: Nginx에서 보안 응답값을 강제 주입했다. 어차피 리버스 프록시 앞에 있으니 add_header로 CSP, HSTS, X-Frame-Options를 넣었다. 15분이면 되는 작업이다. 이걸로 일단 구멍을 막았다.
정규 대응: 다음 주 월요일 정기 배포에 패치 버전 업그레이드를 끼워넣었다. 테스트 파이프라인 돌리고, 스테이징 검증하고, 응답에 보호 설정이 실제로 포함되는지 확인하는 통합 테스트도 하나 추가했다.
교훈 하나
보안 관련 응답 설정은 "코드에 넣었으니 끝"이 아니다. 프레임워크가 삼켜버릴 수 있다. 실제 응답에 해당 값이 붙어서 나가는지 검증하는 테스트가 필요하다. MockMvc로 10줄이면 된다.
mockMvc.perform(get("/"))
.andExpect(header().exists("X-Content-Type-Options"))
.andExpect(header().exists("X-Frame-Options"))
.andExpect(header().exists("Strict-Transport-Security"));
CI에 이거 하나 넣어두면 프레임워크 업그레이드할 때 누락을 바로 잡는다. 이번에 안 터졌으면 영원히 안 넣었을 테스트다.
이번 CVE 해당되는 분들, curl 한 번 찍어보시길.