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

  • STRIDE 여섯 분류를 웹 백엔드 기능에 어떻게 매핑하는가?
  • 같은 기능에서 Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege가 어떻게 다르게 나타나는가?
  • STRIDE 결과를 코드, 테스트, 로그 요구사항으로 어떻게 바꾸는가?

개요

STRIDE는 위협을 여섯 가지 관점으로 나누는 방법이다.

S: Spoofing
T: Tampering
R: Repudiation
I: Information Disclosure
D: Denial of Service
E: Elevation of Privilege

중요한 것은 단어를 외우는 일이 아니다. 새 기능을 볼 때 “이 기능에서는 어떤 사람이 누구인 척할 수 있는가”, “어떤 값을 바꿀 수 있는가”, “나중에 부인할 수 있는가”처럼 질문을 바꾸는 일이다. 웹 백엔드에서는 STRIDE가 Controller annotation보다 더 앞에 있어야 한다. 기능이 어떤 위협을 갖는지 모르면 Spring Security 설정도 어디까지 필요한지 판단하기 어렵다.

공격 시나리오

공격자는 자기 주문 하나를 정상적으로 환불 요청해 본다. 그다음 요청을 바꾼다.

  • orderId를 다른 사용자의 주문으로 바꾼다.
  • amount를 실제 결제 금액보다 크게 넣는다.
  • requestedByRoleSUPPORT_AGENT로 넣는다.
  • 같은 Idempotency-Key 없이 요청을 여러 번 보낸다.
  • webhook endpoint에 성공 결과를 직접 보낸다.
  • 서버 오류 응답에서 결제 transaction id와 내부 상태를 수집한다.

STRIDE로 보면 이 공격은 하나가 아니다. Spoofing, Tampering, Information Disclosure, Denial of Service, Elevation of Privilege가 한 기능 안에서 함께 나타난다.

취약한 요청/응답 예시

POST /orders/30001/refunds HTTP/1.1
Host: api.example.com
Cookie: SESSION=customer-session
Idempotency-Key: attack-001
Content-Type: application/json
 
{
  "userId": 1004,
  "requestedByRole": "SUPPORT_AGENT",
  "amount": 999000,
  "reason": "product issue",
  "force": true
}
HTTP/1.1 200 OK
Content-Type: application/json
 
{
  "refundId": 8821,
  "orderId": 30001,
  "paymentTransactionId": "pay_9w7raw",
  "cardBin": "411111",
  "status": "APPROVED"
}

이 요청은 body의 requestedByRole을 믿는 Spoofing, 환불 금액 조작인 Tampering, 내부 결제 식별자 노출인 Information Disclosure, 고객의 강제 환불 시도인 Elevation of Privilege, 감사 근거 누락인 Repudiation, 중복 외부 호출 위험인 Denial of Service를 동시에 만든다.

주제별 핵심 판단

주문 환불 API를 예로 본다.

feature: order refund
asset:
  - payment transaction
  - refund amount
  - order ownership
  - audit log
actor:
  - customer
  - support_agent
  - payment_webhook
  - admin
entry point:
  - POST /orders/{orderId}/refunds
  - POST /webhooks/payment/refund-result

환불은 금전 상태를 바꾸는 기능이다. 그래서 조회 API보다 더 강한 권한, 멱등성, 감사 로그, 중복 요청 방어가 필요하다.

개선된 요청/응답 예시

분류웹 백엔드 질문환불 API 예시
Spoofing요청 주체가 누구인지 속일 수 있는가?탈취한 세션이나 위조된 webhook으로 환불을 요청한다.
Tampering금액, 소유자, 상태를 조작할 수 있는가?amount, orderId, refundReason을 바꾼다.
Repudiation나중에 누가 했는지 부인할 수 있는가?승인자, 사유, 원 요청 id가 로그에 없다.
Information Disclosure오류나 응답으로 민감 정보가 새는가?다른 사용자의 결제 상태와 카드 일부 정보가 응답된다.
Denial of Service비싼 작업을 반복해 서비스를 압박할 수 있는가?같은 주문에 환불 요청을 반복해 lock과 외부 결제 API 호출을 늘린다.
Elevation of Privilege낮은 권한이 높은 action을 할 수 있는가?고객이 상담원 전용 부분 환불이나 강제 환불을 수행한다.

이 표는 문서 장식이 아니다. 각 줄은 구현과 테스트 항목으로 바뀌어야 한다.

방어 설계

STRIDE 결과는 다음처럼 요구사항으로 바꾼다.

S-1: actor는 session principal 또는 signed webhook 검증 결과에서만 만든다.
T-1: 환불 가능 금액은 서버의 주문/결제 상태로 계산한다.
R-1: 환불 요청, 승인, 거부, webhook 결과는 같은 correlation id로 감사 로그를 남긴다.
I-1: 응답에는 내부 payment transaction 원문과 카드 식별자를 노출하지 않는다.
D-1: 주문별 idempotency key와 rate limit을 적용한다.
E-1: force refund는 support_agent가 아니라 fraud_admin + approval_id가 있을 때만 허용한다.

분류별 요구사항이 있어야 구현 리뷰에서 “어떤 위협을 막는 코드인가”를 확인할 수 있다.

Spring/HTTP 예시

@PostMapping("/orders/{orderId}/refunds")
public ResponseEntity<RefundResponse> requestRefund(
        @AuthenticationPrincipal CustomerPrincipal principal,
        @PathVariable long orderId,
        @RequestHeader("Idempotency-Key") String idempotencyKey,
        @Valid @RequestBody RefundRequest request) {
 
    RefundCommand command = new RefundCommand(
        principal.customerId(),
        orderId,
        request.reason(),
        idempotencyKey);
 
    RefundResult result = refundService.requestCustomerRefund(command);
    return ResponseEntity.accepted().body(RefundResponse.from(result));
}
@Transactional
public RefundResult requestCustomerRefund(RefundCommand command) {
    Order order = orderRepository.findOwnedOrderForUpdate(
        command.orderId(),
        command.customerId()
    ).orElseThrow(() -> new AccessDeniedException("order not found"));
 
    if (!order.canCustomerRequestRefund()) {
        audit.refundDenied(command, "invalid_order_state");
        throw new RefundDeniedException();
    }
 
    Money refundableAmount = refundPolicy.calculateRefundableAmount(order);
    Refund refund = refundRepository.createIfAbsent(
        order.id(),
        command.idempotencyKey(),
        refundableAmount,
        command.reason());
 
    audit.refundRequested(refund.id(), order.id(), command.customerId(), refundableAmount.maskForLog());
    paymentClient.requestRefund(refund.paymentCommand());
 
    return RefundResult.accepted(refund.id());
}

webhook은 고객 session이 아니라 별도 actor다. 서명 검증, event id 멱등성, 허용 event type 확인을 통과한 뒤에만 환불 결과로 반영한다.

운영 로그와 감사 지점

level=INFO event=refund_decision result=accepted stride=S,T,R,D actor_type=customer actor_id=1004 order_id=30001 refund_id=8821 idempotency_key_hash=8d12 reason=customer_request trace_id=21af09c3
level=WARN event=refund_decision result=denied stride=E actor_type=customer actor_id=1004 order_id=30002 reason=ownership_mismatch trace_id=21af09c4
level=WARN event=payment_webhook_denied result=blocked stride=S entry_point=payment_refund_webhook reason=invalid_signature source_ip=203.0.113.51 trace_id=21af09c5

STRIDE 로그는 모든 요청에 여섯 글자를 붙이라는 뜻이 아니다. 민감한 상태 변경에서는 어떤 위협 분류를 다루는 이벤트인지 남기면 사고 분석 때 누락된 방어를 찾기 쉽다.

실전 판단 기준

  • Spoofing: body의 userId, role, force를 바꿔도 principal 기준으로 판단하는가?
  • STRIDE 분류는 각 항목이 테스트, 로그, 완화책 중 하나로 연결될 때만 의미가 있다.
  • 같은 요청도 Spoofing, Tampering, Repudiation이 동시에 걸릴 수 있으므로 하나의 라벨로 끝내지 않는다.

테스트 포인트

  • Tampering: 환불 금액을 임의로 보내면 서버 계산값만 사용되는가?
  • Repudiation: 환불 요청, 승인, 거부, webhook 처리에 correlation id가 연결되는가?
  • Information Disclosure: 응답과 오류에 내부 결제 식별자, 카드 정보, stack trace가 없는가?
  • Denial of Service: 같은 주문과 다른 idempotency key 반복 요청을 제한하는가?
  • Elevation of Privilege: 고객, 상담원, 관리자 각각의 환불 action이 분리되어 있는가?

위험 신호!

  • STRIDE 표만 있고 테스트나 로그 요구사항으로 이어지지 않는다.
  • body에 있는 userId, role, amount, force를 그대로 사용한다.
  • webhook endpoint가 서명 검증 없이 event type만 보고 처리한다.
  • 실패 응답이 결제 provider의 내부 오류와 transaction id를 그대로 노출한다.
  • 멱등성 없이 외부 결제 API를 호출한다.
  • 감사 로그가 성공 이벤트만 남기고 거부와 중복 요청을 남기지 않는다.

확인 질문

확인 질문

  • STRIDE를 웹 백엔드 설계에 쓰는 이유는 무엇인가?
    • 기능별로 다른 실패 모드를 체계적으로 찾고, 이를 구현 요구사항과 테스트, 로그로 바꾸기 위해서다.
  • Tampering과 Elevation of Privilege는 어떻게 구분하는가?
    • Tampering은 값이나 상태 조작이고, Elevation of Privilege는 낮은 권한 actor가 더 높은 action을 수행하는 문제다.
  • Repudiation을 막으려면 무엇이 필요한가?
    • actor, action, asset, decision reason, correlation id, 승인 근거를 감사 로그로 남겨 나중에 행위를 재구성할 수 있어야 한다.

참고 문서