개발 slecs

비회원 결제 후 주문완료 화면이 막히던 문제 수정

목차

비회원으로 결제까지 마쳤는데 주문완료 페이지에서 로그인 화면으로 튕긴다는 제보가 들어왔다. 결제는 성공, 돈은 빠져나갔는데 주문번호를 확인 못 하는 상황. CS 입장에서 제일 짜증나는 케이스다. 결제 성공 = 구매 완료라는 사용자 기대가 충족되지 않으면 그 다음 행선지는 거의 반드시 환불 문의다. 우선순위를 바로 끌어올려서 당일 잡았음.

원인 추적 - 인터셉터가 너무 넓게 닫혀 있었다

비회원 차단 인터셉터가 /order/** prefix 전체를 막고 있었다. 의도는 맞다. 마이페이지, 주문 목록 같은 회원 전용 영역을 보호하려던 거였는데, 결제 직후 리다이렉트되는 주문완료 URL도 같은 prefix 아래 있어서 같이 걸려버렸다.

세션 흐름을 따라가 보면 단순하다.

결제대행사 콜백 → /order/complete?orderId=XXX
↓
차단 인터셉터: 세션에 회원ID 없음 → 로그인 페이지로 강제 이동
↓
사용자: 주문번호 영영 못 봄
  • 비회원 토큰은 세션에 심어둔 상태였는데 인터셉터가 회원ID만 검사하고 있었음
  • 주문 SQL 자체는 회원·비회원 모두 잘 조회됨 (이쪽은 멀쩡)
  • 게이트웨이만 잘못 닫혀있던 전형적인 케이스, 비즈니스 로직은 손댈 필요 없었음

이런 버그가 은근히 자주 나오는 패턴이다. 기능 하나 먼저 만들고, 보안 레이어를 나중에 덧씌울 때 "이 prefix 아래는 다 막아" 식으로 퉁치면 후속 기능이 의도치 않게 잘려나간다. 처음 기능 명세 짤 때 비회원 결제 플로우가 없었거나, 있었어도 인터셉터 작업자한테 충분히 공유가 안 됐던 것.

수정 - 화이트리스트 + 세션 토큰 검증

그냥 /order/complete를 화이트리스트에 추가하면 되는 거 아닌가, 싶지만 그렇게 끝나면 IDOR 구멍이 생긴다. 아무나 /order/complete?orderId=123456 식으로 추측 접근하면 남의 주문이 노출될 수 있다. 그래서 통과 조건을 단순 URL 일치로만 두지 않고, 세션의 비회원 토큰과 주문번호 매핑 검증을 한 단계 끼웠다.

항목 변경 전 변경 후
/order/complete 무조건 차단 세션 토큰 일치 시 통과
/order/list 차단 유지 차단 유지
주문 조회 SQL 회원ID 조건만 비회원 토큰 조건 분기 추가

SQL 쪽도 같이 손봤다. 비회원 주문 조회 시 토큰까지 같이 매칭하도록 WHERE 절을 보강함.

SELECT *
FROM orders
WHERE order_id = :orderId
  AND (
    (member_id = :memberId AND :memberId IS NOT NULL)
    OR
    (guest_token = :guestToken AND member_id IS NULL AND :guestToken IS NOT NULL)
  )

세션 토큰이 안 맞으면 빈 결과 반환. 차단 우회로 노출될 IDOR 가능성도 같이 닫혀서, 결과적으로 이번 수정이 단순 버그픽스 이상이 됐다.

인터셉터 쪽은 화이트리스트를 단순 문자열 목록이 아니라 조건부 통과 방식으로 구조를 잡았다. URL 패턴과 함께 통과 조건 검증 함수를 명시적으로 연결하는 형태다.

// 변경 전 - 단순 경로 화이트리스트
List<String> whiteList = List.of("/order/complete");

// 변경 후 - 경로 + 검증 조건 매핑
Map<String, BiPredicate<HttpServletRequest, HttpServletResponse>> passRules = Map.of(
    "/order/complete", (req, res) -> guestOrderValidator.validate(req)
);

이렇게 해두면 다음에 비슷한 조건부 허용이 필요할 때 검증 함수만 추가하면 되고, 화이트리스트가 뭘 왜 열어두는지 코드 레벨에서 명시적으로 읽힌다.

회고

인터셉터 화이트리스트가 "회원 전용 보호" 관점으로만 설계돼 있었다. 비회원 결제 플로우를 아예 별개 트랙으로 인지 못 했던 게 근본 원인이었고, 그게 이번 버그의 발화점이었다.

보완책으로 앞으로 접근 제어 정책 설계할 때는 코드 작업 전에 트랙별 보호 경로를 표로 먼저 정리해두기로 했다. 최소한 이런 형태라도.

  • 회원 전용 경로 목록
  • 비회원 결제 플로우 경로 목록
  • 비회원 접근 가능하되 본인 소유 검증 필요한 경로 목록
  • 누구나 접근 가능한 경로 목록

화이트리스트가 어디까지 열려야 하는지는 코드만 봐선 잘 안 보인다. 정책 의도가 코드 밖에 명시돼 있어야 리뷰도 되고, 나중에 기능 추가할 때 어디에 넣어야 하는지도 보인다.

결제 성공 직후 화면이 안 보이는 버그는 환불 문의로 곧장 이어진다. 우선순위 끌어올린 건 잘한 판단이었고, 단순 차단 해제에서 끝내지 않고 IDOR까지 같이 닫은 것도 나쁘지 않았다. 급한 불 끄면서 조용히 구멍 막는 게 제일 좋은 핫픽스다.

댓글 0

첫 댓글 달아줘.