Spring Security 설정 실수 7가지: 확인법과 고친 설정
Spring Security 설정에서 점검 때 자주 나오는 실수 7가지를 다룹니다. Actuator 노출, requestMatchers 순서, CORS 와일드카드, 스택트레이스, H2 콘솔을 확인하는 법과 고친 설정을 함께 정리했습니다.
Spring Boot 서비스 점검에서 반복해서 나오는 문제는 대부분 프레임워크 취약점이 아닙니다. 설정 실수입니다. 특히 자주 나오는 것은 Actuator 엔드포인트를 통째로 공개한 경우, 커스텀 SecurityFilterChain을 만든 뒤 Actuator 보호가 사라진 경우, requestMatchers 순서 때문에 관리자 API가 permitAll에 먼저 걸리는 경우, 쿠키 인증과 함께 쓰는 CORS 와일드카드, 에러 응답에 노출되는 스택트레이스, 운영에서 켜진 H2 콘솔, 전체 CSRF 비활성화입니다.
이 글은 개념 설명을 줄이고 각 항목을 확인하는 법 → 고친 설정 순서로 정리했습니다. 우리 프로젝트의 application.yml과 SecurityConfig를 옆에 띄워 두고 하나씩 대조해 보세요. OWASP Top 10 2025에서도 보안 설정 오류(Security Misconfiguration)는 A02로, 2021년판보다 순위가 올라갔습니다.
예시 코드는 Spring Boot 3.x/4.x와 Spring Security 6.x/7.x의 람다 DSL을 기준으로 합니다. 도메인은 모두 가상의 example.com입니다.
1. Actuator 엔드포인트를 *로 전부 공개
Spring Boot 공식 문서에 따르면 HTTP로 기본 공개되는 Actuator 엔드포인트는 health 하나뿐입니다. 문제는 개발 중에 모니터링을 붙이려고 바꾼 설정이 운영에 그대로 남는 경우입니다.
확인하는 법
# 저장소 전체에서 노출 설정 검색
grep -rn "exposure" src/main/resources/
# 우리 서버에서 인증 없이 목록이 보이는지 확인
curl -i https://api.example.com/actuator
인증 없이 env, configprops, heapdump, mappings, loggers 같은 링크가 응답에 나오면 조치 대상입니다. heapdump는 메모리 덤프 파일을 그대로 내려받게 하므로, 메모리에 올라가 있던 토큰이나 DB 비밀번호가 함께 나갈 수 있습니다.
취약한 설정
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
env:
show-values: always
고친 설정
management:
server:
port: 9001 # 관리용 포트를 분리하고 외부 LB/보안그룹에서 차단
endpoints:
web:
exposure:
include: health,info,prometheus # 필요한 것만 명시
endpoint:
env:
show-values: never # 기본값. always로 바꾸지 않는다
공식 문서도 노출 범위를 넓히기 전에 민감 정보가 없는지, 방화벽 뒤에 있는지, Spring Security로 보호되는지를 확인하라고 안내합니다. 키나 비밀번호가 이미 노출됐을 가능성이 있다면 API 키 유출 방지 체크리스트를 참고해 교체 범위부터 정하세요.
2. 커스텀 SecurityFilterChain을 만들고 Actuator 보호가 사라짐
잘 알려지지 않은 함정입니다. 공식 문서에 따르면 Spring Security가 클래스패스에 있고 다른 SecurityFilterChain 빈이 없을 때만 자동 설정이 /health를 뺀 Actuator 전체를 보호합니다. 커스텀 SecurityFilterChain을 정의하는 순간 이 자동 설정은 빠지고, 접근 규칙은 전부 직접 책임져야 합니다.
확인하는 법: SecurityConfig에서 /actuator/**가 permitAll() 목록에 들어 있는지, Actuator 관련 규칙이 아예 없어서 anyRequest()의 규칙에 맡겨져 있는지 확인합니다.
취약한 설정
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/**", "/api/public/**").permitAll()
.anyRequest().authenticated());
고친 설정: Actuator 전용 체인을 분리하고 EndpointRequest로 매칭합니다.
@Bean
@Order(1)
SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
http.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to("health")).permitAll()
.anyRequest().hasRole("OPS"))
.httpBasic(Customizer.withDefaults());
return http.build();
}
EndpointRequest는 management.endpoints.web.base-path를 바꿔도 따라가므로, 경로 문자열을 하드코딩할 때보다 덜 깨집니다.
3. requestMatchers 순서: 넓은 permitAll이 먼저 걸림
Spring Security의 authorizeHttpRequests 규칙은 위에서부터 평가되고, 처음 일치한 규칙이 적용됩니다. 그래서 넓은 패턴의 permitAll()이 좁은 패턴의 hasRole()보다 위에 있으면 아래 규칙은 한 번도 쓰이지 않습니다.
취약한 설정
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll() // 여기서 먼저 매칭
.requestMatchers("/api/admin/**").hasRole("ADMIN") // 도달하지 않음
.anyRequest().authenticated());
고친 설정
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").hasRole("ADMIN") // 좁은 것 먼저
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() // 공개는 메서드까지 한정
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()); // 기본값은 거부
판단 기준
- 규칙은 "좁은 경로 → 넓은 경로" 순서로 둔다.
permitAll()은 허용 목록(allowlist)으로만 쓰고 와일드카드를 최소화한다.- 마지막 줄은
authenticated()나denyAll()로 둔다.anyRequest().permitAll()은 새 API가 추가될 때마다 기본 공개가 된다. - 체인을 여러 개 쓴다면
@Order와securityMatcher범위가 겹치지 않는지도 함께 본다.
확인하는 법: 규칙을 눈으로만 보지 말고 @WebMvcTest나 MockMvc로 "비로그인 사용자가 /api/admin/users를 호출하면 401/403"이 나오는지 테스트로 고정해 두세요. 관리자 기능 전반은 관리자 페이지 보안 체크리스트에서 더 자세히 다룹니다.
4. CORS 와일드카드와 credentials의 조합
Spring은 allowedOrigins("*")와 allowCredentials(true)를 함께 쓰면 설정 단계에서 거부합니다. 그래서 오류를 없애려고 allowedOriginPatterns("*")로 바꾸는 경우가 많은데, 이 설정은 요청한 Origin을 그대로 응답 헤더에 돌려주므로 결과적으로 모든 사이트에 쿠키 포함 요청을 허용하게 됩니다.
확인하는 법
grep -rnE "allowedOrigin(Pattern)?s|@CrossOrigin" src/main/java/
curl -i -H "Origin: https://evil.example.com" https://api.example.com/api/me
응답에 Access-Control-Allow-Origin: https://evil.example.com과 Access-Control-Allow-Credentials: true가 같이 나오면 조치 대상입니다.
취약한 설정
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowCredentials(true);
고친 설정
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"https://www.example.com",
"https://admin.example.com")); // 정확한 출처만
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Content-Type", "Authorization"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
허용 출처는 환경별 설정 파일로 분리하고, localhost는 운영 프로파일에 남기지 않습니다. 컨트롤러마다 붙은 @CrossOrigin이 전역 설정과 다르게 동작하지 않는지도 같이 봅니다.
5. 에러 응답에 스택트레이스·예외 메시지 노출
Spring Boot의 기본 /error 처리는 JSON 클라이언트에 에러 정보를 응답합니다. 기본값은 스택트레이스와 메시지를 숨기는 쪽이지만, 디버깅하려고 always로 바꾼 설정이 운영 프로파일까지 따라가는 경우가 흔합니다. 스택트레이스에는 패키지 구조, 라이브러리 버전, SQL 일부가 담겨 공격자의 정찰을 돕습니다.
확인하는 법: 운영 프로파일에서 include-stacktrace, include-message, include-exception 값을 검색하고, 잘못된 형식의 요청을 보내 응답 본문에 trace나 exception 필드가 있는지 봅니다.
취약한 설정 (Boot 3.x 키 기준)
server:
error:
include-stacktrace: always
include-message: always
include-exception: true
고친 설정
server:
error:
include-stacktrace: never
include-message: never
include-binding-errors: never
include-exception: false
@RestControllerAdvice
class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
ResponseEntity<Map<String, String>> handle(Exception e) {
String traceId = UUID.randomUUID().toString();
log.error("unhandled error traceId={}", traceId, e); // 상세는 서버 로그에만
return ResponseEntity.status(500)
.body(Map.of("code", "INTERNAL_ERROR", "traceId", traceId));
}
}
Spring Boot 4.0에서는 에러 처리 관련 속성이 server 네임스페이스에서 웹 쪽(spring.web.error.*)으로 옮겨졌습니다. 업그레이드했다면 예전 키가 조용히 무시되고 있지 않은지 버전에 맞는 공식 속성 문서로 확인하세요.
6. 운영 환경에서 켜진 H2 콘솔
spring.h2.console.enabled의 기본값은 false이고, 원격 접근(web-allow-others)도 기본은 꺼져 있습니다. 하지만 로컬 개발용으로 켠 설정이 공통 application.yml에 들어가 있거나, 콘솔이 화면에 뜨도록 frameOptions().disable()과 CSRF 예외를 전역으로 걸어 둔 코드가 운영에 남는 경우가 있습니다.
확인하는 법
grep -rn "h2.console\|h2-console\|frameOptions" src/
curl -i https://api.example.com/h2-console
취약한 설정
spring:
h2:
console:
enabled: true
settings:
web-allow-others: true
고친 설정: 콘솔 설정은 application-local.yml에만 두고, H2 의존성 자체를 운영 빌드에서 뺍니다.
# application-local.yml (로컬 전용)
spring:
h2:
console:
enabled: true
dependencies {
// 테스트·로컬에서만 사용
testRuntimeOnly 'com.h2database:h2'
}
H2 콘솔용으로 X-Frame-Options를 끄거나 CSRF를 제외해야 한다면 PathRequest.toH2Console() 범위로만 한정한 별도 체인을 @Profile("local")로 묶으세요.
7. 세션 쿠키 인증인데 CSRF를 전역으로 비활성화
예제 코드에서 흔히 복사되는 csrf(csrf -> csrf.disable())도 점검 단골입니다. 순수 Bearer 토큰 API라면 문제가 적지만, 로그인 상태를 세션 쿠키로 유지하는 웹 서비스에서 CSRF를 끄면 다른 사이트에서 사용자 브라우저로 상태 변경 요청을 보낼 여지가 생깁니다. Spring Boot 문서도 CSRF 비활성화는 브라우저가 아닌 클라이언트만 쓰는 서비스에서 고려하라고 안내합니다.
판단 기준: 인증 수단이 쿠키(JSESSIONID, 쿠키에 담은 JWT 포함)라면 CSRF를 켜 둡니다. 꼭 예외가 필요하면 웹훅처럼 서명으로 별도 검증하는 경로만 ignoringRequestMatchers로 제외합니다.
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/webhooks/payment")); // 서명 검증하는 경로만
쿠키 속성과 세션 설정은 로그인 보안 체크리스트와 함께 점검하면 빠르게 끝납니다.
배포 전 10분 점검 체크리스트
-
management.endpoints.web.exposure.include에*가 없다 -
env,configprops의show-values가always가 아니다 - Actuator가 별도 체인 또는 관리 포트로 분리되어 있고 외부에서 막혀 있다
-
authorizeHttpRequests가 좁은 경로부터 시작하고anyRequest()가authenticated()/denyAll()이다 - 비로그인·일반 사용자로 관리자 API를 호출하는 권한 테스트가 있다
- CORS 허용 출처가 정확한 도메인 목록이며
allowedOriginPatterns("*")가 없다 - 운영 에러 응답에
trace,exception, 내부 메시지가 없다 - H2 콘솔·DevTools 관련 설정이 운영 프로파일과 운영 빌드에 없다
- 쿠키 기반 인증인데 CSRF가 전역 비활성화되어 있지 않다
자주 묻는 질문
Actuator health 엔드포인트도 막아야 하나요?
로드밸런서나 쿠버네티스 프로브가 쓰므로 보통 공개해 둡니다. 다만 상세 정보(show-details)는 never나 when-authorized로 두어 DB·디스크 상태 같은 내부 구성이 비로그인 사용자에게 보이지 않게 하세요.
프로파일 분리만 잘하면 충분하지 않나요?
프로파일은 실수로 local이 운영에서 활성화되거나 공통 파일에 설정이 섞이면 무너집니다. 위험한 기능은 의존성 자체를 운영 빌드에서 빼고, 배포 파이프라인에서 운영 설정을 검사하는 이중 장치를 권합니다.
이런 설정 실수를 자동으로 잡을 수 있나요?
grep 기반 CI 검사(예: exposure.include: "*" 금지), 권한 규칙 통합 테스트, 배포 후 외부에서 /actuator·/h2-console 응답 코드를 확인하는 스모크 테스트 조합이 비용 대비 효과가 좋습니다.
정리
- Spring Boot 보안 사고는 프레임워크 결함보다 Actuator 노출, 규칙 순서, CORS, 에러 응답 같은 설정 실수에서 더 자주 생깁니다.
- 커스텀
SecurityFilterChain을 만들면 Actuator 자동 보호가 꺼진다는 점, 규칙은 처음 일치한 것이 적용된다는 점을 기억하세요. - 개발 편의 설정은 로컬 프로파일과 테스트 의존성 안에만 두고, 권한 규칙은 테스트로 고정합니다.
- 체크리스트를 직접 돌려보기 어렵다면 무료 분석 신청으로 현재 설정을 점검받아 볼 수 있습니다.
참고 자료
- Spring Boot Reference - Actuator Endpoints
- Spring Security Reference - Authorize HttpServletRequests
- Spring Framework Javadoc - CorsConfiguration
- Spring Boot Reference - Servlet Web Applications
- Spring Boot Common Application Properties
- Spring Boot 4.0 Release Notes
- OWASP Top 10 2025 - A02 Security Misconfiguration
함께 읽으면 좋은 글
AWS 보안 설정 가이드: 초기 스타트업이 하루에 잠글 5가지
AWS 보안 설정을 루트 계정, IAM, S3, 보안 그룹, 감사 로그 순으로 정리했다. 소규모 팀이 하루 안에 점검하고 고칠 수 있는 명령어와 수정 전후 예시, 판단 기준을 담았다.
취약점 점검 리포트 읽는 법: 위험도 해석부터 조치 우선순위까지
취약점 점검 리포트의 위험도를 서비스 맥락으로 해석하고, 조치 순서 결정, 오탐 확인, 재점검 요청, 수정 완료 근거 정리까지 개발팀의 처리 흐름을 안내합니다.
개인정보 유출 사고 사례: 개인정보위 처분에서 반복된 원인 5가지
개인정보 유출 사고 사례를 개인정보위 처분 기준으로 분석해 반복된 원인 5가지를 정리하고, 개발팀이 바로 쓸 코드 예시와 점검 항목으로 바꿔 줍니다.