운영 서버의 /healthz/admin 엔드포인트에 인증을 걸어놨는데, 브라우저에서 토큰 없이 접근이 됐다. 처음엔 Spring Security 설정을 잘못 한 줄 알았다. 설정 파일을 세 번 뒤져봐도 문제가 없었다. 알고 보니 Spring Boot Actuator가 인증 정책을 통째로 삼키고 있었다.
Health Group 경로가 인증을 먹는 구조
CVE-2026-22731(CVSS 8.2)의 핵심은 단순하다. Actuator에서 Health Group을 커스텀 경로에 매핑하면, 그 경로 하위의 모든 엔드포인트에 Actuator의 느슨한 보안 정책이 적용된다. Spring Security가 뭘 설정해놨든 상관없이.
management:
endpoint:
health:
group:
liveness:
additional-path: "server:/healthz"
이렇게 설정하면 /healthz는 Actuator health group 경로가 된다. 여기까지는 정상이다. 쿠버네티스 liveness probe 경로를 커스터마이징할 때 흔히 쓰는 패턴이다.
문제는 애플리케이션 코드에서 /healthz/admin 같은 하위 경로에 인증이 필요한 엔드포인트를 만들었을 때 벌어진다. Spring Boot가 /healthz prefix 전체를 Actuator 인프라 경로로 취급하면서, 해당 prefix 아래에 있는 모든 것의 인증 요구사항을 조용히 무시해버린다. SecurityFilterChain에서 .authenticated()를 명시해도, @PreAuthorize를 붙여도 소용없다. Actuator의 경로 매칭이 Spring Security보다 먼저 동작하기 때문이다.
네트워크에서 인증 없이 접근 가능하고, 기밀 데이터 노출 위험이 높다. CVSS 8.2가 괜히 나온 게 아니다.
재현하면 이렇게 된다
@RestController
public class InternalMonitorController {
@GetMapping("/healthz/admin/users")
@PreAuthorize("hasRole('ADMIN')")
public List<UserDto> listUsers() {
return userService.findAll();
}
@PostMapping("/healthz/admin/cache/clear")
@PreAuthorize("hasRole('ADMIN')")
public void clearCache() {
cacheManager.evictAll();
}
}
개발자 입장에서는 합리적인 설계다. health check 관련 경로 아래에 모니터링/관리용 엔드포인트를 모아놓은 것이다. 인증도 걸었다.
$ curl http://localhost:8080/healthz/admin/users
HTTP/1.1 200 OK
[{"id":1,"name":"김철수","email":"cs@company.com"}, ...]
200 OK. 유저 목록이 그냥 나온다. 캐시 초기화 API도 토큰 없이 호출된다. 프로덕션에서 이걸 발견하면 등에서 식은땀이 흐른다.
CloudFoundry 환경은 더 넓게 열린다
CVE-2026-22733도 같은 메커니즘인데 경로가 다르다. CloudFoundry 환경에서 Actuator는 /cloudfoundryapplication을 점유하는데, 이 prefix 아래에 애플리케이션 엔드포인트를 배치하면 동일하게 인증이 증발한다.
"CF 쓰는 팀이 얼마나 되냐"고 할 수 있는데, 대기업 내부 PaaS 중에 CF 기반이 아직 꽤 남아있다. 이런 환경에서 /cloudfoundryapplication 하위에 내부 API를 놓는 경우가 실제로 존재한다. 레거시 인프라일수록 이런 구멍이 오래 방치된다.
영향 범위도 CVE-22731보다 넓다. Spring Boot 2.7.x까지 거슬러 올라간다.
패치 현황
| 브랜치 | 취약 버전 | 패치 버전 |
|---|---|---|
| 4.0.x | 4.0.0 ~ 4.0.3 | 4.0.4 |
| 3.5.x | 3.5.0 ~ 3.5.10 | 3.5.11 |
| 3.4.x | 3.4.0 ~ 3.4.14 | 3.4.15 |
| 2.7.x (CF만) | 2.7.0 ~ 2.7.31 | 2.7.32 |
3월 19일에 전부 릴리스됐다. 2주가 지났는데 아직 안 올렸으면 지금 올려야 한다.
당장 확인할 것
Spring 공식 입장은 "이런 설정이 프로덕션에서 흔하지 않다"이다. 틀린 말은 아니다. 하지만 K8s 환경에서 liveness/readiness probe 경로를 /healthz로 바꾸고, 그 아래에 운영용 엔드포인트를 모아놓는 팀을 나는 여럿 봤다.
점검 순서:
1단계 — 설정 확인. application.yml에서 management.endpoint.health.group 아래에 additional-path가 설정되어 있는지 grep한다. 없으면 CVE-22731은 해당 없다.
2단계 — 경로 충돌 확인. 해당 경로 prefix를 쓰는 @RequestMapping, @GetMapping, @PostMapping이 있는지 찾는다. 있으면 당장 경로를 옮긴다.
3단계 — 버전 올리기. 패치 버전으로 업그레이드한다.
4단계 — 원칙 세우기. Actuator 경로 아래에 비즈니스 엔드포인트를 절대 놓지 않는다. /actuator/*, /healthz/*, /cloudfoundryapplication/* — 전부 인프라 전용이다. 이건 이번 CVE와 관계없이 원래 지켜야 하는 규칙이다.
근데 가이드를 안 읽는 게 우리 업계의 오랜 전통이니까. "동작하니까 됐지" 하다가 CVSS 8.2짜리 구멍이 뚫리는 거다. 동작하는 것과 안전한 것은 다르다.