Project/Web

[Brain404] 07 Spring Boot 경로 탐색 취약점 분석과 우연한 방어의 위험성

우저미 2026. 5. 23. 23:23

안녕하세요! 이번 글에서는 Spring Framework의 정적 리소스 경로 탐색 취약점인 CVE-2024-38816을 실습 환경에서 검증하던 중, 인프라 레벨의 방어 체계와 실무 소스 코드 내에서 발견한 흥미로운 논리적 결함에 대해 공유하고자 합니다. 

취약점 대상 버전이 아님에도 불구하고 실제 서비스 코드에서 어떻게 '위험한 설계'가 방치될 수 있는지, 그리고 왜 이를 보안 검증 단계에서 잡아내야 하는지 상세히 분석해 보았습니다. 

 

1. 실험 환경

취약점 검증을 진행한 모의 인프라 환경은 다음과 같습니다.

- 프록시 서버: Nginx 1.24.0 (Ubuntu)

- 백엔드: Spring Boot 4.0.2 (Spring Framework 7.x 기반, 내장 Tomcat)

- OS: Ubuntu (Linux 환경이므로 /etc/passwd 파일 접근을 타깃으로 설정) 

 

2. CVE-2024-38816 기본 검증

- 평문 ../를 이용한 기본 경로 탈출 시도

먼저 가장 기본적인 형태의 경로 탈출 공격을 수행했습니다. 

 

응답을 보면 403 Forbidden이 출력됩니다. 하지만 헤더를 보면 Vary: Origin, X-Frame-Options 등 Spring Security의 기본 보안 헤더들이 포함되어 있습니다.

이는 StrictHttpFirewall이 공격을 탐지한 것이 아니라는 점입니다. curl은 요청을 보내기 전 클라이언트 사이드에서 .. 경로를 정문화(Normalize)하기 때문에, 서버에는 단순히 GET /etc/passwd로 요청이 도달했습니다.

해당 서버는 /etc/passwd라는 매핑된 핸들러가 없었고, SecurityConfig.anyRequest().authenticated() 인가 규칙에 걸려 일반적인 미인증 응답(403)을 내보낸 것뿐이었습니다. 실제로 ..가 없는 임의의 경로를 호출해도 동일한 응답이 반환됩니다. 

근본적으로 Spring Boot 4.0.2는 Spring Framework 7.x 버전을 사용하고 있어 CVE-2024-38816의 취약 범위(5.3.x, 6.0.x, 6.1.x)를 벗어났으며 패치가 이미 반영된 상태였습니다. 또한 내부적으로 Paths.get(uploadDir).toAbsolutePath().normalize() 처리가 되어 있어 정적 리소스 핸들러 자체는 안전했습니다. 

 

3. 웹 서버와 프레임워크의 4중 방어 레이어 분석

공격자가 클라이언트 정규화를 우회하기 위해 URL 인코딩을 사용했을 때, 외부 인프라(Nginx + Spring Boot)가 이를 어떻게 겹겹이 방어하는지 레이어별로 쪼개보았습니다.

 

- Case 1: 단일 인코딩 (%2F) 우회 시도

결과: 400 Bad Request (Nginx 표준 에러 HTML 반환)

이유 (레이어 ①): Spring 앞단에 위치한 Nginx 1.24.0 리버스 프록시가 단일 인코딩된 슬래시(%2F)를 기본 내장 로직으로 직접 차단합니다. 요청이 아예 Tomcat/Spring까지 도달하지 못합니다.

 

- Case 2: 이중 인코딩 (%252F) 우회 시도

결과: 403 Forbidden (Spring Security 보안 헤더가 완전히 누락된 맨몸 응답)

이유 (레이어 ②): 이중 인코딩된 %252F는 Nginx의 눈에는 슬래시로 보이지 않아 필터를 통과합니다. 하지만 Spring Boot 내부로 들어오는 순간 Spring Security의 StrictHttpFirewall이 2차 디코딩을 수행하면서 내부의 인코딩된 슬래시(%2F)를 감지해 냅니다. 불법적인 요청으로 판단되어 RequestRejectedException이 발생하고, CORS나 필터 레이어를 채 타기 전에 '맨몸 403' 응답을 내던지게 됩니다.

방어 레이어 단계 동작 엔진 공격 형태에 따른 차단 여부 및 응답 특성
① Nginx 프록시 Nginx 1.24.0 단일 인코딩(%2F)을 직접 400 Bad Request HTML로 차단
② StrictHttpFirewall Spring Security 이중 인코딩(%252F) 디코딩 후 차단 (보안 헤더 없는 맨몸 403)
③ 인가 규칙 (Filter) Spring Security 평문 ../ 요청 시 정문화된 경로 미인증으로 403 차단
④ WebMvcConfig 정규화 Spring Web MVC Paths.get().normalize()를 통한 최종 베이스 디렉터리 고정

 

4. 부가 발견: /uploads/<없는파일> 요청 시 500 에러 핸들링 결함

검증 도중 존재하지 않는 파일(GET /uploads/nonexistent-xyz.txt)에 접근했을 때 흥미로운 현상을 발견했습니다. 보통은 404 Not Found가 발생해야 하지만, 시스템은 다음과 같이 JSON 형태의 500 Internal Server Error를 반환했습니다.

- 원인 분석: /uploads/ 경로는 permitAll과 정적 리소스 핸들러로 매핑되어 있어 Spring Security 인증을 무사히 통과합니다. 하지만 핸들러가 내부적으로 존재하지 않는 파일에 접근하는 과정에서 예외가 발생했고, 이를 전역 예외 처리기인 @ControllerAdvice가 캐치하여 일괄적으로 500 에러로 변환해 버린 것입니다. 

- 보안적 관점: 일반 경로(/X) 접근 시에는 403이 떨어지지만, 정적 리소스 경로(/uploads/X) 접근 시에는 500이 떨어집니다. 이 응답 차이를 이용해 외부의 공격자가 "아, 이 서버는 /uploads/ 경로를 정적 리소스 핸들러로 매핑해 쓰고 있구나"라는 사실을 파악(정찰, Reconnaissance)할 수 있으므로, 에러 핸들링을 404로 명확히 정제해 줄 필요가 있습니다. 

 

5. 결론

요약하자면 다음과 같습니다. 

1. 평문 `../` 공격: 서버가 막은 게 아니라 curl 클라이언트가 전송 전에 주소를 스스로 지워버렸고, 존재하지 않는 경로라 Spring Security 인가 규칙(403)에 걸려 우연히 막혔습니다.

2. 단일 인코딩 `%2F` 우회: Spring 부트까지 가지도 못하고, 최전선에 있던 Nginx 프록시가 `400 Bad Request`로 컷했습니다.

3. 이중 인코딩 `%252F` 우회: Nginx는 속여서 통과했지만, Spring Security의 `StrictHttpFirewall`이 내부 디코딩 과정에서 기어코 잡아내며 보안 헤더도 없는 '맨몸 403(아무것도 없는 응답)' 에러를 던졌습니다.

 

결론적으로 테스트한 환경은 CVE-2024-38816 취약점 대상 버전도 아니었을 뿐더러, Nginx ➔ StrictHttpFirewall ➔ 인가 필터 ➔ WebMvcConfig 정규화로 이어지는 4중 방어 레이어 덕분에 인프라 레벨에서는 완벽하게 안전한 상태였습니다.