Skip to content

Commit cc4f4e5

Browse files
committed
fix: NGINX reverse proxy 환경에서 PortOne 웹훅 요청 처리 시 IP 검증 로직 개선
1 parent fde994f commit cc4f4e5

2 files changed

Lines changed: 41 additions & 5 deletions

File tree

app/shop/payment_history/test/webhook_view_test.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,27 @@
1313
_NON_WHITELISTED_IP = "5.6.7.8"
1414

1515

16-
def _post_webhook(*, merchant_uid: str, ip: str, status: str = "paid", imp_uid: str = "imp_x") -> Response:
16+
def _post_webhook(
17+
*,
18+
merchant_uid: str,
19+
ip: str | None = None,
20+
xff: str | None = None,
21+
x_real_ip: str | None = None,
22+
status: str = "paid",
23+
imp_uid: str = "imp_x",
24+
) -> Response:
25+
meta: dict[str, str] = {}
26+
if ip is not None:
27+
meta["REMOTE_ADDR"] = ip
28+
if xff is not None:
29+
meta["HTTP_X_FORWARDED_FOR"] = xff
30+
if x_real_ip is not None:
31+
meta["HTTP_X_REAL_IP"] = x_real_ip
1732
return APIClient().post(
1833
path=reverse("v1:payment_histories-list"),
1934
data=make_webhook_payload(merchant_uid=merchant_uid, status=status, imp_uid=imp_uid),
2035
format="json",
21-
REMOTE_ADDR=ip,
36+
**meta,
2237
)
2338

2439

@@ -64,11 +79,26 @@ def test_ip_allowlist_respects_debug_bypass(
6479
assert not PaymentHistory.objects.filter(order=pending_order).exists()
6580

6681

82+
@pytest.mark.parametrize(
83+
"post_kwargs",
84+
[
85+
# 테스트 환경에서 REMOTE_ADDR 만 직접 지정 (프록시 없음).
86+
{"ip": WEBHOOK_WHITELISTED_IP},
87+
# nginx 뒤 운영: REMOTE_ADDR 은 nginx 컨테이너 IP, 실제 IP 는 X-Forwarded-For 만.
88+
{"ip": _NON_WHITELISTED_IP, "xff": WEBHOOK_WHITELISTED_IP},
89+
# 프록시가 X-Real-IP 만 채우는 변형.
90+
{"ip": _NON_WHITELISTED_IP, "x_real_ip": WEBHOOK_WHITELISTED_IP},
91+
# nginx 가 두 헤더 모두 채우는 일반적인 구성.
92+
{"ip": _NON_WHITELISTED_IP, "xff": WEBHOOK_WHITELISTED_IP, "x_real_ip": WEBHOOK_WHITELISTED_IP},
93+
# 다중 hop X-Forwarded-For — leftmost(원 클라이언트) 가 화이트리스트면 통과.
94+
{"ip": _NON_WHITELISTED_IP, "xff": f"{WEBHOOK_WHITELISTED_IP}, 10.0.0.1, 10.0.0.2"},
95+
],
96+
)
6797
@pytest.mark.django_db
68-
def test_accepts_request_from_whitelisted_ip(mock_portone_find_payment_info, order_factory):
98+
def test_accepts_request_from_whitelisted_ip(post_kwargs, mock_portone_find_payment_info, order_factory):
6999
pending_order = order_factory(status="prepared")
70100
mock_portone_find_payment_info.return_value = make_portone_payment_info(order=pending_order)
71-
response = _post_webhook(merchant_uid=pending_order.merchant_uid, ip=WEBHOOK_WHITELISTED_IP)
101+
response = _post_webhook(merchant_uid=pending_order.merchant_uid, **post_kwargs)
72102
assert response.status_code == HTTP_200_OK
73103
assert response.json() == {"status": "success", "message": "일반 결제 성공"}
74104
assert PaymentHistory.objects.filter(

app/shop/payment_history/views.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
logger = logging.getLogger(__name__)
1414

1515

16+
def _get_client_ip(request: request.Request) -> str | None:
17+
if xff := request.META.get("HTTP_X_FORWARDED_FOR", ""):
18+
return xff.split(",")[0].strip()
19+
return request.META.get("HTTP_X_REAL_IP") or request.META.get("REMOTE_ADDR")
20+
21+
1622
class PaymentHistoryViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
1723
queryset = PaymentHistory.objects.all()
1824
serializer_class = PortOneV1WebhookRequestSerializer
@@ -30,7 +36,7 @@ class PaymentHistoryViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
3036
def create( # type: ignore[override]
3137
self, request: request.Request, *args: typing.Any, **kwargs: typing.Any
3238
) -> response.Response:
33-
if not (settings.DEBUG or request.META.get("REMOTE_ADDR") in settings.PORTONE.ip_list):
39+
if not (settings.DEBUG or _get_client_ip(request) in settings.PORTONE.ip_list):
3440
raise exceptions.PermissionDenied()
3541

3642
logger.info(f"PortOne Webhook Request: {request.data}")

0 commit comments

Comments
 (0)