이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Spring Security를 적용해도 애플리케이션이 직접 책임져야 하는 보안 문제는 무엇인가?
- 입력 검증, 출력 인코딩, 데이터 접근, secret, 로그, abuse detection은 왜 별도 설계가 필요한가?
- “프레임워크가 막아 준다”는 말을 검증 가능한 책임 표로 어떻게 바꾸는가?
개요
Spring Security는 인증과 인가를 위한 강력한 프레임워크지만 애플리케이션의 모든 보안 문제를 자동으로 해결하지 않는다. 프레임워크는 공통 경계를 제공하고, 개발자는 그 경계 위에 안전한 업무 정책과 데이터 정책을 구현해야 한다.
SQL Injection, Mass Assignment, XSS 출력 인코딩, secret 관리, password hashing policy, abuse detection, 개인정보 로그 마스킹, 사고 대응은 Spring Security만으로 끝나지 않는다.
이 장의 결론은 Spring Security를 낮게 평가하자는 것이 아니다. 오히려 Spring Security가 잘하는 일을 명확히 맡기고, 나머지 일을 애플리케이션과 인프라가 명시적으로 책임지게 하자는 것이다.
이 문서의 핵심은 보안 책임을 “사용 중인 기술”이 아니라 “막아야 할 공격과 필요한 증거”로 나누는 것이다.
공격 시나리오
- 공격자는 인증된 사용자로 SQL Injection payload를 검색 API에 넣는다.
- 사용자 프로필 API에
role,point,emailVerified를 추가해 Mass Assignment를 시도한다. - 게시글 제목에 XSS payload를 저장하고 관리자 화면에서 실행되게 만든다.
- reset token과 access token이 로그에 남는지 확인한다.
- 정상 기능을 반복 호출해 OTP 비용과 검색 DB 부하를 만든다.
- Spring Security가 통과시킨 인증 요청 뒤에서 비즈니스 상태 전이를 조작한다.
취약한 요청/응답 예시
@PreAuthorize("isAuthenticated()")
@PatchMapping("/api/users/me")
public UserResponse update(@RequestBody User entity) {
return UserResponse.from(userRepository.save(entity));
}PATCH /api/users/me HTTP/1.1
Authorization: Bearer member-token
Content-Type: application/json
{"nickname":"neo","role":"ADMIN","emailVerified":true}로그인된 사용자라는 사실은 profile API에서 권한 필드를 바꿔도 된다는 뜻이 아니다. 이 취약점은 Spring Security 사용 여부와 별개로 DTO와 domain 경계 문제다.
주제별 핵심 판단
- 인증된 요청도 공격 요청일 수 있다.
- 입력 검증은 DTO, Bean Validation, service validation으로 별도 구현한다.
- 출력 인코딩은 view/API consumer 문맥에 맞게 처리한다.
- SQL Injection 방어는 parameter binding과 query structure allowlist가 맡는다.
- Mass Assignment 방어는 DTO allowlist와 domain method가 맡는다.
- secret과 key는 secret manager, rotation, logging policy가 맡는다.
- abuse detection은 event stream, rate limit, business rule이 맡는다.
개선된 요청/응답 예시
public record MyProfileUpdateRequest(
@NotBlank
@Size(max = 30)
String nickname
) {}@PreAuthorize("isAuthenticated()")
@PatchMapping("/api/users/me")
public UserResponse update(@Valid @RequestBody MyProfileUpdateRequest request,
Authentication authentication) {
MemberId memberId = principalResolver.memberId(authentication);
Member member = memberService.changeNickname(memberId, request.nickname());
return UserResponse.from(member);
}Spring Security는 인증된 사용자인지 확인하고, 애플리케이션은 변경 가능한 필드와 대상 계정을 제한한다.
방어 설계
- 보안 책임 표를 만든다. Spring Security, application service, repository, gateway, database, observability를 나눈다.
- 모든 endpoint에 인증, 인가, 입력 검증, 출력 처리, 로그, rate limit 책임자를 둔다.
- 클라이언트 입력은 보안 결정의 후보일 뿐 근거가 아니라고 정한다.
- DTO allowlist와 domain method를 기본 패턴으로 삼는다.
- query structure는 allowlist, query value는 parameter binding으로 처리한다.
- 로그와 metric에는 민감값을 남기지 않는다.
- secret은 runtime 주입과 rotation 정책으로 다룬다.
- abuse story는 API 설계와 운영 룰에 포함한다.
Spring/HTTP 예시
responsibility:
SpringSecurity:
- authenticate request
- enforce URL and method authorization
- manage SecurityContext
- apply CSRF/CORS/header policies
Application:
- validate business input
- enforce resource ownership
- prevent mass assignment
- emit audit events
Repository:
- bind query parameters
- include tenant/member conditions
Operations:
- manage secrets
- monitor abuse
- retain and protect logs책임 표는 팀 간 핑퐁을 줄인다. 사고가 났을 때도 어느 경계가 깨졌는지 빠르게 좁힐 수 있다.
운영 로그와 감사 지점
level=WARN event=security_responsibility_gap result=blocked api=PATCH /api/users/me gap=mass_assignment owner=team-member traceId=af902c41level=INFO event=security_responsibility_accepted result=allowed api=GET /api/orders/{id} spring=authenticated application=owner_check repository=findByIdAndMemberId traceId=af902c41- 책임 표 변경은 architecture decision record로 남긴다.
- gap이 발견되면 owner와 due date를 둔다.
- Spring Security 설정 변경과 application policy 변경을 같은 release note에 연결한다.
- 보안 사고 후 책임 표를 업데이트한다.
- 반복되는 gap은 template과 test fixture로 자동화한다.
실전 판단 기준
- “Spring Security가 있으니 안전”은 검증 가능한 설명이 아니다.
- authenticated만 확인하면 BOLA/IDOR는 남는다.
- method security가 있어도 DTO와 Repository가 안전해야 한다.
- CORS/CSRF/header는 브라우저 경계 일부를 돕지만 XSS/출력 인코딩을 대체하지 않는다.
- WAF는 generic attack을 줄일 수 있지만 business abuse를 모른다.
- secret과 로그 정책은 Spring Security filter 바깥 운영 책임이다.
테스트 포인트
- 인증된 비소유자가 resource에 접근하지 못하는지 확인한다.
- 예상 밖 DTO 필드가 권한/상태를 바꾸지 못하는지 확인한다.
- SQLi payload가 parameter binding으로 데이터 처리되는지 확인한다.
- XSS payload가 출력 문맥에서 실행되지 않는지 확인한다.
- token과 password가 로그에 남지 않는지 확인한다.
- OTP, reset, search, export API의 abuse limit가 동작하는지 확인한다.
- 책임 표의 각 항목에 테스트 또는 운영 증거가 있는지 확인한다.
위험 신호!
isAuthenticated()만으로 민감 API를 보호한다.- DTO 없이 entity를 직접 받는다.
- Repository 조건에 member/tenant 경계가 없다.
- 출력 인코딩을 Spring Security 책임이라고 설명한다.
- secret 관리와 로그 마스킹 owner가 없다.
- abuse detection을 WAF 또는 rate limit 한 줄로 설명한다.
확인 질문
확인 질문
- Spring Security가 해결하지 않는 대표 문제는 무엇인가?
- 비즈니스 소유권, Mass Assignment, SQL Injection, XSS 출력 인코딩, secret 관리, 로그 마스킹, abuse detection이다.
- 책임 표가 필요한 이유는 무엇인가?
- 프레임워크, 애플리케이션, 데이터, 인프라, 운영의 책임을 분리해 누락과 핑퐁을 줄이기 위해서다.
- 좋은 책임 분리는 어떻게 검증되는가?
- 각 경계마다 테스트, 로그 event, 설정 diff, owner가 있어야 한다.