웹/클라우드/인프라로 돌아가기
웹/클라우드/인프라신윤섭·2026년 7월 4일

웹훅 서명 검증이 뭔데, 안 하면 가짜 결제가 그냥 통과한다

공유

"결제되면 주문 처리하는 기능", 별거 아닌 줄 알았다

서명 검증 없는 웹훅에 공격자가 가짜 결제 성공 요청을 보내 서버를 통과하는 흐름도
서명 검증 없는 웹훅에 공격자가 가짜 결제 성공 요청을 보내 서버를 통과하는 흐름도

Claude Code에게 "스트라이프로 결제가 끝나면 주문을 발송 처리하는 기능 만들어줘"라고 시켰다. 웹훅으로 처리하면 된다면서 코드가 나왔다.

app.post('/webhook', express.json(), (req, res) => {
  const event = req.body
  if (event.type === 'payment_intent.succeeded') {
    fulfillOrder(event.data.object)   // 주문 발송
  }
  res.sendStatus(200)
})

테스트도 잘 된다. 실제로 카드로 결제해 보면 스트라이프가 이 주소로 "결제 성공" 신호를 보내고, 서버가 그걸 받아서 주문을 발송 처리한다. 잘 돌아가니까 배포하고 다음 작업으로 넘어갔다.

문제는 첫 줄에 있다. app.post('/webhook', ...). 이 주소는 인터넷에 그냥 열려 있다. 스트라이프만 여기로 신호를 보낼 수 있는 게 아니다. 이 주소를 아는 사람은 누구나 여기로 요청을 보낼 수 있다. 그리고 저 코드는 요청이 진짜 스트라이프한테서 왔는지 확인하는 부분이 아예 없다. event.typepayment_intent.succeeded이기만 하면 무조건 주문을 발송한다.

즉 공격자가 터미널에서 이 한 줄만 치면 된다.

curl -X POST https://myapp.com/webhook \
  -H "Content-Type: application/json" \
  -d '{"type":"payment_intent.succeeded","data":{"object":{"amount":990000}}}'

돈은 한 푼도 안 냈는데 서버는 "99만 원 결제 성공"으로 알아듣고 상품을 발송한다. 이게 웹훅에서 서명 검증을 빼먹었을 때 벌어지는 일이다.

웹훅은 "일 생기면 이 번호로 전화 줄게"라는 약속이다

먼저 웹훅이 뭔지부터 짚자. 보통 우리가 아는 API 호출은 내가 상대한테 물어보는 방식이다. "결제됐어?"라고 내가 스트라이프에 계속 전화를 거는 것이다. 그런데 언제 결제가 끝날지 모르니 1초마다 전화를 걸 수도 없는 노릇이다.

웹훅은 방향을 뒤집는다. 내가 스트라이프에 미리 주소를 하나 등록해 둔다. "결제가 끝나면 이 주소로 네가 먼저 알려줘." 그러면 일이 생겼을 때 스트라이프가 내 서버로 HTTP POST 요청을 보낸다. 내가 물어보는 게 아니라 상대가 먼저 연락하는 콜백인 셈이다. 결제 완료, 깃허브에 코드가 푸시됨, 문자 메시지가 도착함 같은 "외부에서 일어난 사건"을 실시간으로 받아 처리할 때 거의 항상 웹훅을 쓴다.

편한 만큼 함정이 하나 있다. 그 주소는 스트라이프한테만 알려준 비밀 주소가 아니다. 인터넷에 열린 공개 엔드포인트다. 전화번호를 하나 개통해서 은행에 "결제되면 이 번호로 전화 주세요"라고 등록해 둔 것과 같은데, 문제는 그 번호로 아무나 전화를 걸 수 있다는 것이다. 게다가 발신자 번호는 얼마든지 조작할 수 있다. 보이스피싱범이 은행 대표번호를 사칭해서 거는 것처럼, 공격자도 "나 스트라이프인데 결제 됐어"라고 사칭해서 내 웹훅 주소로 요청을 보낼 수 있다.

전화라면 목소리나 말투로 어렴풋이 의심이라도 하지만, 서버는 그런 감이 없다. 형식만 맞으면 곧이곧대로 믿는다. 그래서 "진짜 스트라이프가 보낸 게 맞는지"를 기계적으로 확인할 방법이 필요하다. 그게 서명 검증이다.

서명은 둘만 아는 도장이다

Stripe와 내 서버가 공유한 비밀키로 같은 HMAC 지문을 계산해 대조하는 서명 검증 원리와 세 가지 함정
Stripe와 내 서버가 공유한 비밀키로 같은 HMAC 지문을 계산해 대조하는 서명 검증 원리와 세 가지 함정

서명 검증의 원리는 생각보다 간단하다. 웹훅을 등록하면 스트라이프가 서명용 비밀키를 하나 준다(whsec_...로 시작하는 값이다). 이 값은 스트라이프와 나만 안다. 공격자는 모른다.

스트라이프는 요청을 보낼 때마다 이렇게 한다. 보낼 내용 전체에 이 비밀키를 섞어서 HMAC이라는 방식으로 지문 같은 값을 하나 계산하고, 그 값을 요청 헤더에 같이 실어 보낸다. HMAC은 "비밀키 + 내용"을 넣으면 항상 똑같은 결과가 나오지만, 비밀키를 모르면 그 값을 절대 만들어낼 수 없는 계산법이다. 내용이 한 글자만 바뀌어도 결과가 완전히 달라진다.

내 서버는 요청을 받으면 똑같은 계산을 한다. 내가 가진 비밀키로, 받은 내용에 대해 HMAC을 직접 계산해 본다. 그 결과가 헤더에 실려 온 값과 같으면 진짜 스트라이프가 보낸 것이다. 다르면 사칭이니 그냥 버린다. 공격자는 비밀키가 없으니 아무리 그럴듯한 요청을 만들어도 헤더에 맞는 값을 채워 넣지 못한다.

도장 찍힌 서류를 떠올리면 된다. 위조범이 서류 내용은 똑같이 베껴 쓸 수 있어도, 진짜 인감도장이 없으면 도장은 못 찍는다. 서명 검증은 요청 하나하나에 대해 "이 도장이 진짜인가"를 확인하는 일이다. 스트라이프, 깃허브, 쇼피파이, 트윌리오 같은 웹훅을 보내는 서비스는 거의 다 이 HMAC 방식으로 서명을 붙여 보낸다. 받는 쪽에서 확인만 안 할 뿐이다.

AI 에이전트에 웹훅을 붙이면 판돈이 더 커진다

여기까지는 가짜 결제로 공짜 물건을 빼가는 이야기였다. 요즘 바이브 코딩으로 많이 만드는 것 중 하나가 문자나 채팅을 받아서 처리하는 AI 에이전트인데, 여기에 웹훅이 붙으면 위험의 성격이 달라진다.

실제 사례가 있다. 오픈소스 AI 에이전트 hermes-agent는 트윌리오로 문자를 받아 처리하는 기능이 있었는데, 문자가 도착했다고 알려주는 웹훅에서 X-Twilio-Signature 헤더를 검증하지 않았다. 즉 트윌리오가 보낸 진짜 문자인지 확인을 안 했다. 공격자는 그 웹훅 주소로 가짜 문자 도착 요청을 보내면서 발신 번호(From)를 관리자 번호로 위조할 수 있었다. 에이전트는 그 번호를 믿고 문자 내용을 명령으로 받아 실행했다. 결국 공격자가 문자 내용에 명령을 심어서 서버에서 원하는 코드를 실행하는 데까지 이어졌다(원격 코드 실행).

구조를 보면 앞의 결제 사례와 똑같다. 검증 안 된 웹훅 입력을 그대로 믿는다는 것. 다만 결제 웹훅은 그 값이 "주문을 발송할지 말지"로 흘러갔고, AI 에이전트는 그 값이 "무슨 명령을 실행할지"로 흘러간다. 에이전트는 웹훅으로 들어온 내용을 그대로 사용자 명령으로 취급하는 경우가 많아서, 위조된 입력 하나가 곧바로 도구 실행으로 이어진다. 신원(누가 보냈는지)을 검증 안 된 입력에서 뽑아 쓰는 순간, 그 뒤에 붙은 권한 체크가 통째로 무의미해진다.

Codex나 Claude Code로 "슬랙 메시지 오면 처리하는 봇", "고객 문의 문자 받아서 답하는 에이전트" 같은 걸 만든다면 전부 이 구조다. 입구가 웹훅이고, 그 입구에 서명 검증이 없으면 아무나 그 봇에게 명령을 내릴 수 있는 셈이다.

AI가 짜준 코드는 검증을 아예 빼거나, 넣어도 어설프다

문제는 두 갈래다. 첫째, AI에게 웹훅 핸들러를 시키면 서명 검증을 아예 빼놓는 경우가 흔하다. 동작하는 데는 검증이 없어도 아무 지장이 없으니, "일단 돌아가는 코드"를 만들면 자연스럽게 빠진다. 테스트할 때는 나도 공격자도 진짜 요청만 보내니 문제가 안 드러난다.

둘째, 검증을 넣으라고 시켜도 미묘하게 틀리는 지점이 있다. 특히 세 가지를 자주 놓친다.

원본 요청 본문(raw body) 문제가 제일 흔하다. 서명은 서버로 들어온 바이트 그대로를 기준으로 계산해야 맞는다. 그런데 위 예제처럼 express.json()을 먼저 걸어두면, 검증하기도 전에 본문이 JSON 객체로 파싱되면서 원본 바이트가 사라진다. 그 상태로 서명을 계산하면 값이 절대 안 맞는다. 그러면 십중팔구 이런 일이 벌어진다. 검증이 자꾸 실패하니까 답답한 나머지 검증 코드를 통째로 주석 처리해 버린다. 스트라이프 웹훅은 express.raw({ type: 'application/json' })로 원본을 그대로 받아서 검증부터 하고, 그 다음에 파싱해야 한다.

문자열 비교 방식도 있다. 계산한 서명과 받은 서명을 그냥 ===로 비교하면 안 되고 crypto.timingSafeEqual 같은 상수 시간 비교 함수를 써야 한다. 일반 비교는 앞부분이 맞을수록 미세하게 시간이 더 걸리는데, 공격자가 이 시간 차이를 측정해서 서명을 한 글자씩 알아맞힐 여지를 준다. 다행히 스트라이프 공식 함수를 쓰면 이 부분을 알아서 처리해 준다.

마지막은 재전송(replay)이다. 서명이 맞더라도 그건 "진짜 스트라이프가 언젠가 보낸 진짜 요청"이라는 뜻일 뿐이다. 공격자가 정상 요청 하나를 어깨너머로 가로챘다면, 그걸 그대로 며칠 뒤에 다시 보내도 서명은 여전히 유효하다. 그래서 서명 안에 들어 있는 시간값을 같이 확인해서, 5분쯤 지난 오래된 요청은 거부해야 한다. 이것도 공식 검증 함수가 대체로 챙겨준다.

그래서 어떻게 막나

가장 확실한 방법은 직접 HMAC을 계산하겠다고 나서지 않는 것이다. 웹훅을 보내는 서비스는 거의 다 공식 검증 함수를 제공한다. 스트라이프면 stripe.webhooks.constructEvent, 깃허브나 트윌리오도 각자 방식이 있다. 이 함수 하나가 서명 확인, 상수 시간 비교, 시간값 확인까지 웬만한 함정을 다 막아준다. 앞의 코드는 이렇게 고치면 된다.

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature']
  let event
  try {
    event = stripe.webhooks.constructEvent(
      req.body,                          // 파싱 안 한 원본 그대로
      sig,
      process.env.STRIPE_WEBHOOK_SECRET  // whsec_... 는 환경변수에
    )
  } catch (err) {
    return res.sendStatus(400)           // 서명 안 맞으면 여기서 끝
  }

  if (event.type === 'payment_intent.succeeded') {
    fulfillOrder(event.data.object)
  }
  res.sendStatus(200)
})

핵심은 순서다. 서명 검증이 핸들러의 첫 관문이어야 한다. 검증을 통과하기 전에는 event.type을 들여다보지도, 주문을 처리하지도 않는다. 검증에 실패하면 거기서 400으로 끊고 아무 일도 하지 않는다.

서명용 비밀키(whsec_...)는 코드에 박아두지 말고 환경변수에 넣는다. 이게 유출되면 공격자가 진짜 서명을 만들어낼 수 있으니, API 키와 똑같이 취급해야 한다. 이 부분은 환경변수와 API 키 관리 글에서 다룬 원칙 그대로다.

AI 에이전트에 웹훅을 붙였다면 한 가지가 더 있다. 서명 검증을 통과했다는 건 "트윌리오가 보낸 게 맞다"까지만 보장한다. 그 안에 실린 발신 번호나 메시지 내용이 정말 믿을 만한 사람의 것인지는 별개 문제다. 검증된 요청이라도 그 안의 값으로 곧바로 도구를 실행하지 말고, 누가 보낸 명령인지 다시 한번 확인하는 단계를 두는 게 안전하다.

정리

웹훅 주소는 스트라이프만 아는 비밀 통로가 아니라 인터넷에 열린 공개 문이다. 그 문으로 들어온 요청이 진짜 스트라이프가 보낸 것인지 확인하지 않으면, 누구나 "결제 완료"나 "관리자가 보낸 명령"을 위조해서 밀어 넣을 수 있다. 서명 검증은 요청마다 도장이 진짜인지 확인하는 일이고, 다행히 대부분의 서비스가 검증 함수를 만들어 놓았으니 우리는 그 함수를 핸들러 맨 앞에 두기만 하면 된다. AI에게 웹훅 핸들러를 시켰다면 검증 코드가 들어 있는지, 그게 핸들러의 첫 줄인지, 원본 본문을 파싱 전에 검증하는지부터 확인하는 게 시작점이다.

YS

신윤섭

데이너스 대표 | AI 교육 & AX 컨설팅

81개 이상의 AI/AX 교육 과정을 설계하고, 50여 기업과 기관에서 강의했습니다. 강남세브란스, 삼성전자, 현대자동차 등 다양한 조직의 AI 역량 강화를 지원하고 있습니다.

AI 교육이 필요하신가요?

조직에 맞는 맞춤형 AI/AX 교육 프로그램을 설계해드립니다. 커리큘럼 상담부터 시작해보세요.

같은 주제의 다른 글