이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 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=af902c41
level=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가 있어야 한다.

참고 문서