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

  • 공격자는 웹 서비스를 어떤 순서와 관점으로 탐색하는가?
  • 정상 기능을 악용하는 요청과 단순한 비정상 요청은 어떻게 다른가?
  • Java/Spring 백엔드에서는 공격자 관점을 어떤 테스트와 로그로 바꿔야 하는가?

개요

공격자 관점은 “나쁜 사람이 어떤 해킹 도구를 쓰는가”를 상상하는 일이 아니다. 정상 사용자가 볼 수 있는 화면, API, 오류, 응답 시간, 권한 차이를 따라가며 서버가 어떤 값을 신뢰하는지 추론하는 방식이다.

백엔드 개발자는 기능을 Controller 단위로 보지만 공격자는 입력면, 권한면, 상태 변경면, 오류면, 운영면으로 본다. 검색창은 입력면이고, 주문 상세 URL은 권한면이며, 결제 취소는 상태 변경면이다. 500 응답과 자세한 오류 메시지는 오류면이고, 로그 누락은 운영면이다.

공격자 관점으로 보는 목적은 겁을 주기 위한 것이 아니라 테스트 케이스를 만들기 위해서다. 공격자가 바꿀 값을 미리 알면, 방어 설계와 회귀 테스트도 구체화된다.

공격 시나리오

공격자는 보통 다음 순서로 서비스를 본다.

  • 인증 없이 접근 가능한 URL, 정적 파일, API 문서, 오류 페이지를 확인한다.
  • 회원가입과 로그인을 하며 응답 메시지 차이로 계정 존재 여부를 추론한다.
  • 로그인 후 자신의 리소스를 조회하고 URL, JSON field, hidden field, query parameter를 바꿔 본다.
  • 상태 변경 API를 새 탭, 다른 Origin, 오래된 token, 낮은 빈도 반복 요청으로 호출해 본다.
  • 오류 메시지와 응답 시간을 비교해 내부 검증 순서와 DB 조건을 추론한다.

이 흐름에서 공격자는 “관리자 버튼이 보이는가”보다 “관리자 API가 서버에서 막히는가”를 본다. UI는 힌트일 뿐 방어선이 아니다.

취약한 요청/응답 예시

PATCH /api/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer member-token
Content-Type: application/json
 
{
  "nickname": "neo",
  "role": "ADMIN",
  "accountLocked": false
}
HTTP/1.1 200 OK
Content-Type: application/json
 
{
  "id": 1004,
  "nickname": "neo",
  "role": "ADMIN"
}

공격자는 “내 프로필 수정” 기능을 권한 상승 기능으로 바꿨다. UI에는 role 입력칸이 없어도 JSON 필드를 추가할 수 있다.

주제별 핵심 판단

  • 공격자는 기능 이름보다 입력면, 권한면, 상태 변경면, 오류면, 운영면을 먼저 본다.
  • 정상 기능을 더 빠르게, 더 많이, 다른 순서로 호출하는 것도 공격이 될 수 있다.
  • abuse case는 취약점 이름보다 실제 피해 경로를 더 잘 드러낸다.
  • 로그인, 검색, 업로드, 결제, 관리자 API는 공격자가 먼저 지도에 올리는 경로다.
  • 공격자 관점은 개발자의 테스트 목록으로 번역되어야 의미가 있다.

개선된 요청/응답 예시

public record UpdateMyProfileRequest(
    @Size(max = 30) String nickname,
    @Size(max = 200) String bio
) {}
 
@PatchMapping("/api/users/me")
public ProfileResponse updateMe(@Valid @RequestBody UpdateMyProfileRequest request,
                                Authentication authentication) {
    Long memberId = ((CustomPrincipal) authentication.getPrincipal()).memberId();
    return profileService.updateMyProfile(memberId, request);
}

요청 DTO는 사용자가 바꿀 수 있는 필드만 가진 allowlist다. role, accountLocked, point, emailVerified 같은 보안 결정 필드는 별도 관리자 API와 승인 흐름으로 분리해야 한다.

방어 설계

  • 각 기능마다 공격자가 바꿀 수 있는 값을 목록화한다.
  • 요청 DTO는 허용 필드만 가진다. Entity를 요청 본문으로 직접 받지 않는다.
  • 클라이언트에서 숨긴 값과 서버에서 거부한 값을 구분한다.
  • 상태 변경 API는 인증, 권한, 입력 검증, 사용자 의도 확인, 감사 로그를 함께 본다.
  • 오류 메시지는 행동 안내 수준으로 줄이고 내부 class, SQL, stack trace를 숨긴다.
  • rate limit는 로그인, 인증번호, 검색, 결제처럼 endpoint 비용과 피해가 다른 곳에 다르게 둔다.
  • 보안 테스트는 exploit 성공 여부가 아니라 “정상 기능을 악용할 수 있는가”를 확인한다.

Spring/HTTP 예시

@ControllerAdvice
class ApiExceptionHandler {
 
    @ExceptionHandler(AccessDeniedException.class)
    ResponseEntity<ErrorResponse> accessDenied(AccessDeniedException ex) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(new ErrorResponse("ACCESS_DENIED"));
    }
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    ResponseEntity<ErrorResponse> invalidInput(MethodArgumentNotValidException ex) {
        return ResponseEntity.badRequest()
            .body(new ErrorResponse("INVALID_INPUT"));
    }
}

공격자에게 내부 검증 순서를 알려 주지 않으면서도 서버 로그에는 traceId와 세부 reason을 남긴다. 클라이언트 응답과 운영 로그는 목적이 다르다.

운영 로그와 감사 지점

level=WARN event=unexpected_field_rejected actor=member:1004 path=/api/users/me field=role result=blocked traceId=42a9
level=WARN event=authorization_denied actor=member:1004 resource=admin:user-role action=update result=blocked reason=not_admin traceId=42a9

공격자 관점의 로그는 “실패”가 아니라 “어떤 악용 시도가 있었는지”를 보여 주어야 한다.

  • 예상 밖 JSON field를 기록하되 값 전체를 남기지는 않는다.
  • 계정별 실패, IP별 실패, endpoint별 실패를 분리해 본다.
  • 관리자 API 접근 실패와 일반 입력 검증 실패는 다른 이벤트로 남긴다.
  • 반복 실패 후 성공한 요청은 별도 탐지 대상이다.
  • 테스트 환경에서 만든 공격 요청과 운영 공격 시도를 로그 필드로 구분한다.

실전 판단 기준

  • “프론트에서 숨겼다”는 방어가 아니라 UI 표현일 뿐이다.
  • Postman이나 curl로 UI가 보내지 않는 필드를 직접 추가해 테스트해야 한다.
  • 기능 리뷰에서는 성공 플로우보다 abuse case를 먼저 물어본다.
  • 오류 응답, 로그, rate limit가 같은 정책을 바라보는지 확인한다.
  • 좋은 판단은 공격자가 다음에 무엇을 시도할지 예측해 탐지 지표로 바꿀 때 나온다.

테스트 포인트

  • 요청 JSON에 role, isAdmin, ownerId, price, accountLocked를 추가했을 때 거부되는지 확인한다.
  • 다른 사용자의 resource id를 넣었을 때 403 또는 정책상 404가 나오는지 확인한다.
  • 같은 실패를 여러 IP와 여러 계정으로 반복했을 때 rate limit와 탐지가 동작하는지 확인한다.
  • 오류 응답에 내부 class, SQL, stack trace가 없는지 확인한다.
  • 보안 이벤트가 endpoint, actor, resource, reason 기준으로 집계 가능한지 확인한다.

위험 신호!

  • “그 필드는 프론트에서 안 보내요”를 방어 논리로 사용한다.
  • 요청 DTO와 Entity가 거의 같은 구조다.
  • 관리자 API와 사용자 API가 같은 DTO를 공유한다.
  • 403, 400, 429가 모두 같은 로그 이벤트로 남는다.
  • 실패 요청의 필드명과 endpoint가 남지 않아 공격 패턴을 볼 수 없다.

확인 질문

확인 질문

  • 공격자 관점으로 기능을 볼 때 가장 먼저 확인할 것은 무엇인가?
    • 공격자가 바꿀 수 있는 입력값, 리소스 식별자, 상태 변경 순서, 오류 응답이다.
  • UI에서 숨긴 필드가 방어가 아닌 이유는 무엇인가?
    • HTTP 요청은 클라이언트가 직접 만들 수 있으므로 서버가 DTO와 권한 정책으로 거부해야 하기 때문이다.
  • abuse case는 왜 필요한가?
    • 정상 기능을 악용하는 경로를 테스트와 로그 요구사항으로 바꾸기 위해서다.

참고 문서