Webhook 서명 검증은 외부에서 도착한 HTTP 요청이 진짜 발신자(Stripe·GitHub·PortOne 등)에게서 왔는지 HMAC-SHA256으로 확인하는 인증 절차다.
AI 코딩 도구가 만든 결제·CI·알림 통합 코드에서 가장 흔히 생략되는 보안 분기이며, 누락 시 공격자가 위조 요청 하나로 결제 완료를 가짜로 트리거하거나 CI 파이프라인을 탈취할 수 있다.
이 글은 Stripe·PortOne·GitHub Webhook의 표준 검증 코드, 5가지 자주 발생하는 실수, 그리고 30초 자가진단 + 24시간 패치 절차를 정리했다.
한눈에 보는 Webhook 서명 검증
Q. Webhook 서명 검증을 안 하면 어떤 일이 벌어지나요? A. 공격자가 결제 완료 Webhook을 위조해 무료로 상품을 받거나, CI Webhook을 위조해 임의 코드를 main 브랜치에 배포할 수 있습니다. HTTPS만으로는 발신자 인증이 안 됩니다. Q. HTTPS면 충분하지 않나요? A. HTTPS는 통신 경로 암호화만 보장합니다. 공격자가 자기 서버에서 위조 Webhook을 직접 호출하면 HTTPS도 정상 동작합니다. 서명 검증은 페이로드 + 비밀키 기반 HMAC으로 발신자 신원을 확인합니다. Q. 30초 안에 검증 누락을 확인할 수 있나요? A. 자기 Webhook 엔드포인트에 임의 페이로드 + Signature 헤더 없이 POST를 보내고 200 OK가 떨어지면 검증 누락입니다. 401·400이 떨어져야 정상입니다. Q. 검증 코드는 어디에 작성해야 하나요? A. 모든 비즈니스 로직(주문 처리·DB 업데이트) 이전에 미들웨어나 데코레이터로 강제해야 합니다. 라우트 핸들러 본문에 두면 누락 위험이 큽니다.5가지 자주 발생하는 실수 패턴
| # | 패턴 | 예시 | 위험도 |
|---|---|---|---|
| 1 | 검증 자체 생략 | Stripe/PortOne 비밀키 사용 안 함, 페이로드만 신뢰 | 치명 |
| 2 | raw body 대신 parsed body 사용 | JSON.parse 후 다시 stringify 한 값으로 HMAC 계산 → 서명 불일치 또는 우회 | 치명 |
| 3 | 타이밍 어택 가능한 비교 | signature == calculated 일반 비교 사용 (constant-time 미적용) | 높음 |
| 4 | timestamp 미검증 | 오래된 서명을 재전송(replay)해도 통과 | 높음 |
| 5 | 비밀키 하드코딩·노출 | 코드에 박혀 GitHub 공개 → 분 단위 자동 봇이 수집 | 치명 |
표준 검증 코드 — Stripe (Python·Django)
import hmac
import hashlib
import time
from django.http import HttpResponseBadRequest
from django.conf import settings
def verify_stripe_webhook(request):
sig_header = request.headers.get('Stripe-Signature', '')
raw_body = request.body # ★ JSON.parse 전 raw bytes 필수
secret = settings.STRIPE_WEBHOOK_SECRET
# 1. 헤더 파싱: "t=1234567890,v1=abc..."
parts = dict(p.split('=', 1) for p in sig_header.split(','))
timestamp = parts.get('t')
signature = parts.get('v1')
if not timestamp or not signature:
return HttpResponseBadRequest('서명 헤더 누락')
# 2. timestamp 5분 이내 검증 (replay 차단)
if abs(int(time.time()) - int(timestamp)) > 300:
return HttpResponseBadRequest('서명 만료')
# 3. HMAC-SHA256 계산
payload = f"{timestamp}.{raw_body.decode()}"
expected = hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
# 4. constant-time 비교 (타이밍 어택 방지)
if not hmac.compare_digest(expected, signature):
return HttpResponseBadRequest('서명 불일치')
return None # 검증 통과
핵심 4요소: (1) raw body 사용 (2) timestamp 검증 (3) HMAC-SHA256 (4) constant-time 비교.
하나라도 빠지면 결함이다. AI 코딩 도구가 만든 코드에서는 ①④가 가장 자주 누락된다.
미들웨어로 강제해 라우트별 누락 위험을 원천 차단하는 것이 표준이다.
무료 등급 결과 + 17개 스캐너 통합 진단.
PortOne (한국 결제) 검증 표준
PortOne V2는 결제 완료 Webhook에 Webhook-Signature 헤더를 동봉한다.
검증 알고리즘은 Stripe와 거의 동일하나, 한국 환경 특이점이 있다.
- 비밀키는 PortOne 콘솔 → 채널 → Webhook 설정에서 발급
- 페이로드는 raw bytes 그대로, JSON 재직렬화 금지
- 서버검증 API(
/payments/{paymentId}) 호출로 이중 확인 권장 — Webhook은 보조, 결제 상태는 결제 검증 API가 진실
한국 PG 결제 위변조 사고 사례는 SaaS 결제 통합 보안 함정 — PortOne·Toss 가이드에서 별도 정리했다.
GitHub Actions / Webhook 검증 표준
GitHub Webhook은 X-Hub-Signature-256 헤더로 HMAC-SHA256 서명을 전달한다. GitHub 공식 가이드에 따른 표준 검증:
def verify_github_webhook(request):
sig = request.headers.get('X-Hub-Signature-256', '')
secret = settings.GITHUB_WEBHOOK_SECRET
expected = 'sha256=' + hmac.new(
secret.encode(),
request.body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
return HttpResponseBadRequest('GitHub 서명 불일치')
CI Webhook 검증 누락 시 임의 페이로드로 배포 트리거가 가능하다.
공격자는 자기 컨테이너 빌드 이미지를 main에 흘릴 수 있고, 비밀 환경변수가 빌드 로그에 누설된다.
관련: 공급망 공격 시즌2 — npm CI/CD 5점검.
알림 SaaS (Slack·Discord) 서명 검증
Slack은 X-Slack-Signature + X-Slack-Request-Timestamp 조합, Discord는 Ed25519 공개키 서명을 사용한다. 알고리즘이 달라도 4요소 원칙(raw body·timestamp·HMAC/공개키·constant-time)은 동일하다.
알림 Webhook이 위조되면 운영팀에 가짜 장애 알림을 흘려 실제 사고를 가리는 사회공학 공격이 가능하다.
2025년 Discord 봇 위장 사례에서 동일 패턴이 확인됐다.
OWASP A07: 식별·인증 실패 범주에 해당한다.
30초 자가진단 — 새고 있는지 즉시 확인
다음 3가지 명령으로 1차 점검 가능. 하나라도 200 OK가 떨어지면 검증 누락이다.
# 1. 서명 헤더 없이 호출
curl -X POST https://yourdomain.com/webhook/stripe -H "Content-Type: application/json" -d '{"type":"payment_intent.succeeded","amount":100000}'
# 2. 위조 서명으로 호출
curl -X POST https://yourdomain.com/webhook/stripe -H "Stripe-Signature: t=1234567890,v1=fakefakefake" -d '{"type":"payment_intent.succeeded"}'
# 3. 오래된 timestamp로 호출 (1시간 전)
TS=$(($(date +%s) - 3600))
curl -X POST https://yourdomain.com/webhook/stripe -H "Stripe-Signature: t=$TS,v1=anysig" -d '{"type":"payment_intent.succeeded"}'
정상 응답은 모두 400 또는 401이어야 한다.
발견 시 24시간 패치 절차
| 순위 | 조치 | 이유 |
|---|---|---|
| 1 | 비밀키 회전 — 콘솔에서 즉시 재발급 | 이미 유출됐을 가능성 차단 |
| 2 | 검증 함수 작성 + 미들웨어로 강제 | 모든 Webhook 엔드포인트 일괄 적용 |
| 3 | raw body 처리 확인 — body-parser 미들웨어 순서 점검 | parsed body 사용 시 서명 불일치 |
| 4 | timestamp + replay 차단 (5분 윈도우) | 재전송 공격 차단 |
| 5 | 로그에 검증 실패 시각·IP 기록 | 공격 시도 모니터링 |
| 6 | 결제 Webhook은 결제 검증 API 이중 확인 | Webhook 자체에 의존하지 않음 |
지금 해야 할 것 — 30초 자가진단 + 1:1 상담
실무자라면: 자기 Webhook 엔드포인트 URL을 CodeScan 무료 스캔에 입력 → TLS·헤더·노출 패턴 자동 점검. 검증 누락 시 위 표준 코드로 패치.
CISO·임원이라면: Webhook 검증은 자동 스캔으로 부분만 잡힌다. 실제 페이로드 위조 시뮬레이션은 수동 점검이 필요하다. SENTRIX 30분 1:1 보안 상담 신청 → 결제·CI·알림 통합 전반의 인증 결함을 함께 점검한다.