외부 결제 PG사나 알림 서비스 등을 연동하면서 웹훅(Webhook) 수신 엔드포인트를 만들어본 경험이 있을 것이다.
다만 대부분의 개발자들은 URL만 노출되어 있으면 누구나 위조된 HTTP 요청을 보낼 수 있다는 위험성을 간과한 채, 수신된 데이터를 그대로 DB에 반영하곤 한다.
이번에는 웹훅 위변조 공격이 왜 위험한지, 이를 방지하기 위한 HMAC SHA256 서명 검증이 정확히 무엇인지, 그리고 어떻게 실무에 적용하는지 완벽하게 정리해서 소개하겠다.

 

1. 웹훅 보안과 HMAC SHA256 서명 검증의 개념

웹훅은 서버 대 서버 통신 방식으로, 결제 완료나 상태 변경 같은 이벤트가 발생했을 때 지정된 URL로 HTTP POST 요청을 날려주는 메커니즘이다.
문제는 이 수신 URL이 공용 인터넷에 열려있는 엔드포인트라는 점이다. 해커나 악의적인 사용자가 해당 URL을 알아내어 임의로 {"status": "PAID", "amount": 100000} 같은 페이로드를 POST로 보낸다면, 검증 로직이 없을 경우 결제도 하지 않은 주문이 처리될 수 있다.

이를 방지하기 위해 사용되는 표준적인 기술이 바로 **HMAC(Hash-based Message Authentication Code)**이다.
송신자와 수신자가 오직 둘만 알고 있는 '비밀키(Secret Key)'를 공유하고, 송신자는 전달할 페이로드 데이터와 비밀키를 조합하여 HMAC SHA256 해시값을 생성한 뒤 헤더에 담아 보낸다. 수신자는 동일한 페이로드와 비밀키로 해시를 직접 재계산하여 전달받은 서명과 일치하는지 확인하는 방식이다.

 

2. 웹훅 검증 동작 메커니즘 비교

웹훅 서명 검증 방식이 기존 단순 인증 방식과 어떻게 다른지 한눈에 비교해 보자.

구분IP 허용 목록(IP Whitelisting)단순 API 토큰 검증HMAC SHA256 서명 검증
방식발송 서버의 IP 대역만 수신 허용요청 헤더의 고정 토큰값 확인비밀키 + 페이로드 본문 기반 동적 해시 검증
장점설정이 간편함구현이 단순함데이터 위변조 차단 및 완전한 수신자/송신자 인증
단점발송측 IP 변경 시 장애 발생, 프록시 우회 가능성토큰 탈취 시 모든 요청 위조 가능서버단에서 서명 계산 비용 소폭 발생
보안성보통낮음매우 높음 (추천)

 

3. 실전 웹훅 수신 및 서명 검증 예제

실제 외부 API 서비스에서 보내오는 웹훅 요청을 받아서 검증하는 PHP 실전 예제 코드를 작성해 보자.
수신 헤더에는 X-Signature라는 이름으로 HMAC SHA256 해시 값이 전달된다고 가정한다.

✗ 잘못된 연동 코드 (검증 없이 페이로드 직렬화 및 처리):

<?php
// 위험: 요청 데이터가 진짜 서비스 제공자가 보낸 것인지 검증하지 않음
$data = json_decode(file_get_contents('php://input'), true);

if (isset($data['event']) && $data['event'] === 'payment.success') {
    // DB 업데이트 실행 (위조된 요청에 취약)
    updateOrderStatus($data['order_id'], 'PAID');
}
?>

✓ 올바른 연동 코드 (HMAC SHA256 검증 및 타임 공격 방지 적용):

<?php
// 1. 서버에 안전하게 보관 중인 웹훅 비밀키
$secretKey = 'my_super_secret_webhook_key_12345';

// 2. HTTP 헤더에서 서명(Signature) 추출
$headers = getallheaders();
$receivedSignature = $headers['X-Signature'] ?? $headers['x-signature'] ?? '';

// 3. 원본 Raw Body 수신 (json_decode 이전에 읽어야 함)
$rawPayload = file_get_contents('php://input');

if (empty($rawPayload) || empty($receivedSignature)) {
    http_response_code(400);
    echo json_encode(['error' => 'Bad Request: Missing payload or signature']);
    exit;
}

// 4. 수신한 Raw Body와 비밀키로 HMAC SHA256 서명 재계산
$expectedSignature = hash_hmac('sha256', $rawPayload, $secretKey);

// 5. hash_equals() 함수로 시간 공격(Timing Attack) 방지하며 비교
if (!hash_equals($expectedSignature, $receivedSignature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized: Invalid signature']);
    exit;
}

// 6. 서명 검증 성공 후 비즈니스 로직 수행
$data = json_decode($rawPayload, true);
if (($data['event'] ?? '') === 'payment.success') {
    // 안전하게 처리 진행
    updateOrderStatus($data['order_id'], 'PAID');
}

http_response_code(200);
echo json_encode(['success' => true]);
?>

실행 결과 / 출력값 (서명이 유효하지 않거나 위조된 경우):

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"error": "Unauthorized: Invalid signature"}

 

4. 실무에서 자주 하는 실수와 주의사항

웹훅 서명 검증을 구현할 때 백엔드 개발자들이 흔히 범하는 몇 가지 치명적인 실수가 있다.

실수 1: json_decode 후 다시 json_encode 한 문자열로 서명 검증하기

송신 측이 생성한 서명은 전송된 '원본 문자열(Raw String)' 기준이다. 수신측에서 json_decode()를 진행한 뒤 다시 json_encode()를 거치면 띄어쓰기, 줄바꿈, 유니코드 이스케이프 등이 변경되어 전혀 다른 해시값이 생성된다.
따라서 반드시 file_get_contents('php://input')으로 읽은 원본 바이트 스트림을 그대로 hash_hmac()에 전달해야 한다.

실수 2: 단순 동등 비교자(== 또는 ===) 사용하기

문자열 검증 시 $expectedSignature === $receivedSignature 와 같이 일반 비교 연산자를 사용하면 '타이밍 공격(Timing Attack)'에 노출될 수 있다. 문자열 비교 연산자는 앞자리부터 일치 여부를 검사하다가 틀린 지점에서 바로 리턴되므로, 응답 시간에 미세한 차이가 생긴다.
반드시 hash_equals() 함수를 사용하여 항상 일정한 시간 동안 비교하도록 처리해야 안전하다.

 

5. 요약 및 마무리

웹훅 서명 검증은 외부 서비스와 데이터를 주고받는 시스템에서 결코 타협할 수 없는 필수 보안 요소다. 작은 원본 검증 습관이 모여서 서비스 전체의 신뢰성과 데이터 무결성을 만든다는 점을 잊지 말자. 이 글의 실전 예제 코드를 참고해 수신 엔드포인트에 HMAC SHA256 검증 로직을 구현하면, 위변조된 악성 요청으로부터 서비스 데이터를 완벽하게 보호할 수 있을 것이다.