웹훅은 타사 서비스로부터 실시간 업데이트를 받는 가장 강력한 방법 중 하나입니다. Stripe, GitHub, Shopify 또는 Twilio에서 전송되는 단일 HTTP POST는 고객에게 요금을 청구하거나, 저장소를 업데이트하거나, 주문을 배송하거나, 확인 SMS를 보내는 등 애플리케이션의 중요한 비즈니스 로직을 트리거할 수 있습니다.
하지만 모든 웹훅 요청은 공용 인터넷을 통해 전달됩니다. 이는 웹훅 URL을 추측하거나 발견한 사람은 누구든지 완전히 합법적으로 보이는 악성 페이로드를 보낼 수 있다는 것을 의미합니다. 적절한 인증 없이는 애플리케이션이 실제 이벤트와 위조된 이벤트를 구분할 방법이 없습니다.
바로 이 지점에서 웹훅 서명 검증이 필요합니다. 이는 모든 수신 웹훅 요청이 예상하는 서비스에서 실제로 온 것이며 전송 중에 변경되지 않았음을 보장하는 간단하고 표준화된 메커니즘입니다.
이 포괄적인 가이드에서는 웹훅 서명 검증이 정확히 어떻게 작동하는지, 그리고 인기 있는 언어에서 이를 올바르게 구현하는 방법을 배울 것입니다. 또한 피해야 할 일반적인 실수와 모든 것을 빠르고 안정적으로 엔드투엔드 테스트하는 방법도 알게 될 것입니다.
웹훅 서명 검증이란 무엇인가요?
웹훅 서명 검증은 수신되는 웹훅 요청이 예상하는 서비스에서 실제로 왔으며 변조되지 않았음을 확인하는 과정입니다.
대부분의 제공업체는 SHA-256 또는 SHA-512와 함께 HMAC(Hash-based Message Authentication Code)를 사용합니다. 서비스는 다음과 같이 계산합니다.
signature = HMAC-SHA256(secret_key, payload)
그런 다음 헤더(일반적으로 X-Signature, Signature 또는 X-Hub-Signature-256)에 서명을 보냅니다.
귀하의 서버는:
- 페이로드를 원시 바이트로 수신합니다 (중요!)
- 저장된 비밀을 사용하여 HMAC를 다시 계산합니다
- 계산된 서명을 수신된 서명과 비교합니다
정확히 일치하면 웹훅을 처리합니다. 그렇지 않으면 HTTP 401 또는 403을 반환합니다.
HMAC-SHA256이 업계 표준인 이유
제공업체들이 HMAC-SHA256을 선택하는 데는 다음과 같은 좋은 이유가 있습니다:
- 빠름: 보통의 하드웨어에서도 매우 빠릅니다.
- 안전함: 2025년에도 SHA-256은 깨지지 않았습니다.
- 간단함: 하나의 비밀 키, 관리할 공개/개인 키 쌍이 없습니다.
- 표준화됨: 모든 언어의 라이브러리가 이를 올바르게 구현합니다.
GitHub, Stripe, Shopify, Slack 및 수십 개의 다른 서비스들이 모두 HMAC-SHA256을 사용합니다.
Node.js에서 웹훅 서명 검증을 구현하는 방법
Node.js의 실제 예시부터 시작하겠습니다.
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
const computedSignature = hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(signature)
);
}
주요 유의 사항:
- 타이밍 공격을 방지하기 위해 항상
crypto.timingSafeEqual을 사용하세요. - 문자열에
===을 사용하지 마세요. 취약합니다. - 일정 시간 비교를 보장하기 위해
Buffer.from()을 사용하세요.
Express 미들웨어 예시:
app.post('/webhooks/stripe', (req, res, next) => {
const signature = req.headers['stripe-signature'];
const secret = process.env.STRIPE_WEBHOOK_SECRET;
// Get raw body (Express needs middleware to preserve raw body)
const rawBody = req.rawBody || req.body; // use body-parser with verify option
if (!verifyWebhookSignature(rawBody, signature, secret)) {
return res.status(401).send('Invalid signature');
}
// Signature is valid → process the event
next();
});
Python 구현 (FastAPI + Pydantic)
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
app = FastAPI()
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, signature)
@app.post("/webhooks/github")
async def github_webhook(request: Request):
signature = request.headers.get("X-Hub-Signature-256")
if not signature:
raise HTTPException(status_code=401, detail="Missing signature")
payload = await request.body()
if not verify_signature(payload, signature.split('=')[1], SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
# Process webhook
return {"status": "ok"}
개발자들이 흔히 저지르는 실수 (및 피하는 방법)
1. JSON.stringify() 또는 파싱된 본문 사용
많은 프레임워크는 JSON을 자동으로 파싱합니다. 이는 공백, 키 순서 및 서식이 다르기 때문에 검증을 깨뜨립니다.
해결책: 항상 파싱하기 전에 원시 본문을 캡처하세요.
Express에서: { verify: true }와 함께 body-parser를 사용하세요.
FastAPI에서: await request.body()를 사용하세요.
2. ===로 문자열 비교
타이밍 공격은 정보를 유출할 수 있습니다. crypto.timingSafeEqual 또는 hmac.compare_digest를 사용하세요.
3. 코드에 비밀 저장
환경 변수 또는 비밀 관리자(AWS Secrets Manager, HashiCorp Vault 등)를 사용하세요.
4. 재실행 공격 처리 누락
대부분의 제공업체는 타임스탬프를 포함합니다. 이벤트가 최근 것인지 확인하세요(예: 5분 이내).
const timestamp = req.headers['X-Signature-Timestamp'];
if (Date.now() - timestamp > 5 * 60 * 1000) {
return res.status(401).send('Timestamp too old');
}
5. SHA-1 사용 (여전히 발생!)
GitHub는 2022년에 SHA-1을 더 이상 사용하지 않도록 했습니다. 항상 SHA-256을 사용하세요.
Apidog로 웹훅 서명 검증 테스트
웹훅을 수동으로 테스트하는 것은 고통스러운 일입니다. 요청을 보내고, 로그를 확인하고, 수정하고, 반복해야 합니다.
Apidog는 이것을 사소한 일로 만듭니다:
Apidog 프로젝트에서 왼쪽 사이드바의 + 아이콘을 클릭하고 "새 기타 프로토콜 API" > "웹훅"을 선택하세요.
웹훅을 생성한 후 편집기에서 다음 필드를 작성하세요:요청 메서드: 일반적으로POST.웹훅 이름: 이는 API 문서 및 OpenAPI 내보내기에 표시됩니다 (예: order).디버그 URL (선택 사항): 테스트 요청을 보내는 데 사용되는 실제 URL. 참고: 이것은 테스트 목적으로만 사용되며 문서에 포함되지 않습니다.기타 정보: 요청 본문과 같은 정보.
모든 필수 필드를 작성한 후 저장을 클릭하세요.
웹훅 URL을 디버그 URL 필드에 입력한 다음, 보내기를 클릭하여 웹훅 호출을 시뮬레이션하세요.
Apidog의 웹훅 시뮬레이터 덕분에 디버깅 시간을 몇 시간이나 절약했습니다. Stripe의 정확한 stripe-signature 형식과 GitHub의 sha256=... 접두사까지 지원합니다.
실제 예시: Stripe 웹훅 검증
Stripe는 특별한 헤더 형식을 사용합니다:
stripe-signature: t=1681234567,v1=abc123...,v0=def456...
다음과 같이 해야 합니다:
t=타임스탬프를 추출합니다v1=서명(선호됨)을 추출합니다payload + timestamp를 사용하여 다시 계산합니다v1부분만 비교합니다
Stripe는 이러한 복잡성을 처리하기 위한 공식 라이브러리를 제공합니다:
const stripe = require('stripe')('sk_...');
stripe.webhooks.constructEvent(payload, sigHeader, endpointSecret);
하지만 직접 구현해야 할 때는 기본 HMAC를 이해하는 것이 중요합니다.
고급 주제: 여러 서명 허용
일부 제공업체(예: Stripe)는 하위 호환성을 위해 여러 서명을 보냅니다. 귀하의 코드는 다음을 수행해야 합니다:
- 헤더를
,로 분할합니다 - 각각을 시도합니다
- 일치하는 것이 있으면 수락합니다
2025년 보안 모범 사례
- 웹훅 비밀을 90일마다 교체하세요.
- 가능하면 단기 비밀을 사용하세요.
- 실패한 검증은 기록하되, 비밀은 절대 기록하지 마세요.
- 무차별 대입 공격을 방지하기 위해 웹훅 엔드포인트에 속도 제한을 적용하세요.
- 항상 HTTPS를 사용하세요.
결론: 작은 검증 단계, 큰 보안 이득
웹훅 서명 검증은 사소한 세부 사항처럼 들릴 수 있습니다. 하지만 이는 안전한 애플리케이션과 공격자가 쉽게 악용할 수 있는 애플리케이션의 차이를 만듭니다.
이를 올바르게 구현하고, Apidog와 같은 도구로 철저히 테스트하며, 통합이 보호된다는 사실을 알고 더 편안하게 잠드세요.
오늘 Apidog를 무료로 다운로드하고 5분 안에 첫 웹훅을 검증하세요. 이는 귀하의 코드가 실제로 작동함을 증명하는 가장 빠른 방법입니다.
