diff --git a/build.gradle b/build.gradle index b03f8ba..f054695 100644 --- a/build.gradle +++ b/build.gradle @@ -94,7 +94,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' developmentOnly 'org.springframework.boot:spring-boot-devtools' - implementation 'org.springframework:spring-expression:5.3.30' + // spring-expression 개별 pin 제거: 아래 ext 의 spring-framework.version 이 전 모듈을 일괄 관리한다. + // 개별 pin 이 남아 있으면 다른 spring-* 모듈보다 낮은 버전으로 고정되어 버전이 어긋난다. implementation group: 'xalan', name: 'xalan', version: '2.7.3' @@ -176,6 +177,17 @@ ext { // 주의: 3.1 은 #request/#session/#response/#servletContext 표현식 객체를 제거했다(IllegalArgumentException). set('thymeleaf.version', '3.1.4.RELEASE') set('thymeleaf-extras-springsecurity.version', '3.1.5.RELEASE') + + // Spring Framework 5.3.x OSS 마지막 릴리스로 통일(Boot 2.7.18 BOM 기본 5.3.31, 일부 5.3.30 혼재였음). + // 5.3.32~5.3.39 구간 CVE 정리용. 5.3.x 는 OSS EOL 이라 이 위로는 상용(Enterprise) 빌드뿐이다. + // 남는 5.3.x 지적(CVE-2026-41855 JMS Jackson 역직렬화, CVE-2016-1000027 HttpInvoker)은 + // 5.3 계열에 수정본이 없고 앱이 JMS/HttpInvoker 를 쓰지 않으므로 억제 항목으로 따로 관리한다. + set('spring-framework.version', '5.3.39') + + // Spring Security 5.7.x OSS 마지막 릴리스(Boot 2.7.18 BOM 기본 5.7.11). + // CVE-2026-22732(응답 커밋 후 보안 헤더 미기록)는 수정본이 5.7.22/5.8.24 = Enterprise 전용이라 + // OSS 로는 올릴 수 없다. 우회책(HeaderWriterFilter.shouldWriteHeadersEagerly=true)은 동작 변경이라 분리 검토. + set('spring-security.version', '5.7.14') } diff --git a/ci/dependency-check-suppressions.xml b/ci/dependency-check-suppressions.xml index e70259b..f4e941e 100644 --- a/ci/dependency-check-suppressions.xml +++ b/ci/dependency-check-suppressions.xml @@ -28,4 +28,94 @@ --> + + + + + + ^pkg:maven/org\.springframework\.boot/.*@2\.7\..*$ + CVE-2026-40974 + + + + + + ^pkg:maven/org\.springframework\.boot/spring-boot-starter-thymeleaf@.*$ + CVE-2026-40477 + CVE-2026-40478 + + + + + + ^pkg:maven/org\.springframework/spring-.*@5\.3\..*$ + CVE-2026-41855 + + + + + + ^pkg:maven/org\.springframework/spring-web@5\.3\..*$ + CVE-2016-1000027 + + + + + + ^pkg:maven/org\.springframework\.security/spring-security-.*@5\.7\..*$ + CVE-2026-22732 + + diff --git a/src/main/java/com/eactive/apim/portal/config/PortalConfigSecurity.java b/src/main/java/com/eactive/apim/portal/config/PortalConfigSecurity.java index 533419b..9365cbf 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalConfigSecurity.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalConfigSecurity.java @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -16,6 +17,7 @@ import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandlerImpl; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.csrf.CsrfException; +import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -92,6 +94,23 @@ public class PortalConfigSecurity { http .authenticationManager(portalAuthenticationManager) + // CVE-2026-22732 우회책. Spring Security 는 기본적으로 보안 헤더를 "응답 커밋 시점"에 + // 지연 기록하는데, 응답 래퍼가 Content-Length 를 setHeader/setIntHeader/addIntHeader 로 + // 지정하는 경로를 추적하지 못해 그 경우 헤더가 통째로 누락된다 + // (X-Content-Type-Options, X-Frame-Options, Cache-Control, Pragma, Expires, X-XSS-Protection). + // 수정본은 5.7.22/5.8.24(Enterprise 전용)뿐이라 OSS 로는 올릴 수 없어 우회책을 적용한다. + // 요청 시작 시점에 헤더를 기록하게 만든다. 앱이 나중에 같은 헤더를 지정하면 앱 값이 남는다 + // (실측: FileDownloadController#viewImage 의 Cache-Control: public, max-age=86400 유지됨). + // 현재 앱은 response.setContentLength(int) 만 쓰므로 노출 경로는 없지만, + // 새 코드가 위 메서드를 쓰더라도 헤더가 빠지지 않도록 두는 안전망이다. + // Spring Security 를 수정본(6.5.9+/7.0.4+ 또는 Enterprise 5.7.22+)으로 올리면 제거 가능. + .headers(headers -> headers.addObjectPostProcessor(new ObjectPostProcessor() { + @Override + public O postProcess(O filter) { + filter.setShouldWriteHeadersEagerly(true); + return filter; + } + })) .formLogin(form -> form .loginPage("/login") .usernameParameter("id")