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

  • API 보안 테스트는 성공 케이스 외에 어떤 실패 케이스를 반드시 포함해야 하는가?
  • 인증, 인가, 입력 검증, 브라우저 경계, abuse 검증은 각각 어떤 증거를 남겨야 하는가?
  • 자동 테스트와 수동 취약점 검증의 역할은 어떻게 나뉘는가?

개요

보안 테스트는 happy path를 확인하는 테스트가 아니다. 공격자는 정상 요청에서 일부 값만 바꾼다. 따라서 테스트는 인증 없음, 권한 없음, 비소유자 resource, 만료 token, 예상 밖 필드, 잘못된 Origin, 반복 호출을 먼저 넣어야 한다.

Mock test만으로 충분하지 않을 수 있다. Spring Security FilterChain, Method Security, CORS, CSRF, exception handler는 통합 테스트에서 실제로 적용되는지 봐야 한다.

취약점 검증은 payload가 막히는지만 보지 않는다. 실패 응답 status, 오류 메시지, 보안 로그, alert, rate limit event까지 함께 검증해야 운영에서 탐지 가능하다.

이 문서의 핵심은 테스트 항목을 “실패해야 하는 요청”과 “남아야 하는 증거”로 쓰는 것이다.

공격 시나리오

  • 공격자는 다른 사용자의 order id를 넣어 GET, PATCH, DELETE를 시도한다.
  • 만료된 access token과 재사용 refresh token으로 session boundary를 시험한다.
  • JSON body에 role, ownerId, approved, price 같은 예상 밖 필드를 추가한다.
  • CORS preflight와 CSRF token이 빠진 상태 변경 요청을 보낸다.
  • SQLi, XSS, open redirect, SSRF payload를 정상 parameter에 넣는다.
  • rate limit 아래에서 낮은 빈도 반복 호출로 abuse 탐지를 회피한다.

취약한 요청/응답 예시

PATCH /api/orders/9001 HTTP/1.1
Host: api.example.com
Authorization: Bearer member-2002
Content-Type: application/json
 
{"status":"CANCELLED"}
test=OrderControllerTest.updateOrder status=passed
covered=owner_success_only
missing=non_owner, expired_token, unexpected_field, audit_log

성공 케이스만 통과하면 실제 보안 경계는 검증되지 않았다.

주제별 핵심 판단

  • 인증 테스트는 없음, 만료, 위조, 잘못된 issuer/audience를 포함한다.
  • 인가 테스트는 객체 수준과 기능 수준을 분리한다.
  • 입력 테스트는 타입, 길이, enum, 업무 의미, 예상 밖 필드를 포함한다.
  • 브라우저 테스트는 CORS, CSRF, SameSite, CSP 영향을 본다.
  • API abuse 테스트는 rate limit와 business flow 제한을 본다.
  • 오류 테스트는 status와 response body의 정보 노출을 본다.
  • 탐지 테스트는 로그 event와 alert 발생 여부를 본다.

개선된 요청/응답 예시

@Test
void nonOwnerCannotCancelOrder() throws Exception {
    mockMvc.perform(patch("/api/orders/{id}", orderOwnedByOtherMember)
            .with(jwt().jwt(jwt -> jwt.subject("member-2002")))
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"status\":\"CANCELLED\"}"))
        .andExpect(status().isNotFound());
 
    assertThat(auditEvents).anyMatch(event ->
        event.name().equals("authorization_denied") &&
        event.reason().equals("owner_mismatch"));
}

테스트는 status만 보지 않고 보안 이벤트가 설계대로 남는지도 확인한다.

방어 설계

  • API별 security test matrix를 만든다.
  • 각 endpoint에 인증, 권한, 입력, 오류, 로그, abuse test를 연결한다.
  • 통합 테스트에서 실제 FilterChain과 Method Security를 켠다.
  • negative test는 성공 test와 같은 수준으로 필수화한다.
  • 테스트 데이터는 owner, non-owner, admin, locked user, deleted resource를 포함한다.
  • 취약점 scanner 결과는 수동 재현과 false positive 판정을 남긴다.
  • 테스트 실패가 배포 gate와 연결되게 한다.
  • 운영 탐지 룰은 staging에서 synthetic event로 검증한다.

Spring/HTTP 예시

@SpringBootTest
@AutoConfigureMockMvc
class OrderSecurityTest {
 
    @Test
    void anonymousCannotCreateRefund() throws Exception {
        mockMvc.perform(post("/api/refunds")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"orderId\":9001,\"amount\":1000}"))
            .andExpect(status().isUnauthorized());
    }
}
@Test
void unexpectedRoleFieldIsRejected() throws Exception {
    mockMvc.perform(patch("/api/users/me")
            .with(jwt().jwt(jwt -> jwt.subject("member-1004")))
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"nickname\":\"neo\",\"role\":\"ADMIN\"}"))
        .andExpect(status().isBadRequest());
}

Mock security helper를 쓰더라도 실제 security configuration이 로드되는지 확인한다.

운영 로그와 감사 지점

level=INFO event=security_test_evidence_recorded result=allowed api=PATCH /api/orders/{id} test=OrderSecurityTest.nonOwnerCannotCancelOrder evidence=authorization_denied traceId=7a0e12d1
level=WARN event=vulnerability_validation_gap result=blocked api=POST /api/refunds missing=abuse_rate_limit_test owner=team-payment due=2026-07-05 traceId=7a0e12d1
  • 보안 테스트 누락은 PR 상태나 품질 gate에 반영한다.
  • 수동 검증 결과에는 payload, 기대 결과, 실제 결과, 증거 로그를 남긴다.
  • scanner false positive도 이유와 owner를 기록한다.
  • staging synthetic attack은 운영 alert와 같은 경로를 타는지 확인한다.
  • 테스트 matrix는 API inventory와 동기화한다.

실전 판단 기준

  • @WebMvcTest만 사용하면 보안 설정 일부가 빠질 수 있다.
  • 성공 케이스만 많으면 권한 누락을 발견하지 못한다.
  • 403 대신 200 false를 반환하면 클라이언트와 탐지가 혼란스러울 수 있다.
  • scanner 통과는 business logic authorization 검증을 대체하지 않는다.
  • 로그 이벤트 검증이 없으면 운영 탐지 실패가 배포 후에 드러난다.
  • 테스트 데이터가 단일 사용자뿐이면 IDOR을 놓친다.

테스트 포인트

  • 인증 없음, 만료 token, 잘못된 issuer/audience를 테스트한다.
  • 비소유자 resource 조회/수정/삭제가 실패하는지 확인한다.
  • 권한 없는 role이 관리자 action을 수행할 수 없는지 확인한다.
  • 예상 밖 JSON 필드와 mass assignment payload를 넣는다.
  • SQLi/XSS/open redirect/SSRF payload를 입력값에 넣는다.
  • CORS/CSRF/SameSite 관련 브라우저 흐름을 검증한다.
  • rate limit와 abuse rule이 event와 함께 동작하는지 확인한다.

위험 신호!

  • 테스트 이름이 모두 success 또는 happyPath다.
  • 보안 실패 응답 status를 검증하지 않는다.
  • audit log나 security event를 테스트하지 않는다.
  • scanner 결과만 있고 수동 재현 기록이 없다.
  • 운영 alert 경로가 staging에서 검증되지 않았다.
  • 테스트 데이터에 non-owner와 admin이 없다.

확인 질문

확인 질문

  • 보안 테스트의 기본 단위는 무엇인가?
    • 성공 요청이 아니라 실패해야 하는 공격 요청과 그 증거다.
  • 통합 테스트가 필요한 이유는 무엇인가?
    • FilterChain, Method Security, CORS, CSRF, exception handler가 실제로 연결되는지 보기 위해서다.
  • 취약점 검증의 완료 증거는 무엇인가?
    • payload, 기대 결과, 실제 응답, 보안 로그, alert 또는 rate limit event다.

참고 문서