Webhook を安全に使うための完全ガイド
─ 署名検証・リプレイ攻撃対策をわかりやすく解説
Webhook は「外部サービスから HTTP リクエストを受け取る仕組み」です。
便利ですが、設計を間違えると誰にでも叩かれてしまうため、セキュリティを意識しない運用はとても危険です。
この記事では
- Webhook が危険な理由
- 安全に使うための原則
- HMAC 署名の実装例(Python)
- タイムスタンプ検証 / nonce / IP 制限
- 実運用での注意点
を、初心者でも理解できる形でまとめました。
これはgakuseibotのアドベントカレンダー三日目の記事です。是非お読みください!!
目次
-
Webhook はなぜ危険になりやすい?
-
安全に使うための必須ポイント
-
HMAC 署名検証の実装(Python)
-
リプレイ攻撃を防ぐ方法
-
IP 制限は「補助」でしかない理由
-
Webhook 設計でやってはいけないこと
-
まとめ
1. Webhook はなぜ危険になりやすい?
Webhook は 「インターネットから受け取る HTTP POST」 というだけの仕組みです。
つまり…
URL が漏れたら誰でも叩ける
Webhook の URL は パスワードではありません。
ランダム文字列にしても、
URL がログに残った
誤って公開した
誰かが推測した
などで漏れる可能性があります。
正規の送信者かどうか判別できない
HTTP リクエストを送るのは誰でも可能なので
「本当にサービスから送られたリクエストなのか?」
を検証しなければいけません。
過去のリクエストを再送する「リプレイ攻撃」
正しい署名をもつリクエストでも、
同じデータを悪意ある第三者が再送する
=「リプレイ攻撃」が起きる可能性があります。
2. Webhook を安全に使うための必須ポイント
Webhook を安全にするために必要なのは以下の3つです。
① HMAC 署名で送信元を検証する
最も重要な方法。
秘密鍵(secret)を使って送信者が署名し、受信側で同じ計算をして一致を確認する。
→ 正しい送信者しか正しい署名を作れない
② タイムスタンプや nonce で「使い回し」を防ぐ
・一定時間以内(例:±5分)だけ受け付ける
・nonce を保存して一度使われたものは拒否する
→ リプレイ攻撃を防ぐ
③ HTTPS を必ず使う
HTTP だと盗聴されるため、署名ごと漏れます。
- HMAC 署名検証の実装例(Python)
ここでは、サービス側が以下を送ってくる想定とします
ヘッダー:X-Signature(HMAC-SHA256)
ヘッダー:X-Timestamp
ボディ:JSON
実装は完全に一般的な HMAC の仕組みで、特定サービス固有の仕様は含みません。
import hmac
import hashlib
import json
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b"your_webhook_secret" # 自分だけが知っている秘密鍵
def verify_signature(timestamp: str, body: bytes, signature: str) -> bool:
"""
署名が正しいか検証する関数
"""
message = timestamp.encode() + body
expected_signature = hmac.new(
SECRET,
message,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, signature)
@app.route("/webhook", methods=["POST"])
def webhook():
timestamp = request.headers.get("X-Timestamp")
signature = request.headers.get("X-Signature")
body = request.get_data()
# 必須ヘッダーがなければ拒否
if not timestamp or not signature:
abort(400)
# 署名検証
if not verify_signature(timestamp, body, signature):
abort(401) # Unauthorized
data = request.json
print("Valid webhook received:", data)
return "ok", 200
if __name__ == "__main__":
app.run(port=3000)
ポイント
hmac.compare_digest() を使うことでタイミング攻撃対策になる
署名は secret を知っている送信者だけが作れる
timestamp + body を使うことで「タイムスタンプ検証」が可能
3. リプレイ攻撃を防ぐ方法
HMAC だけでは「使い回しの再送」を防げません。
必ず以下どちらかを使います。
方法①:タイムスタンプの時間差チェック
例:
「送信時刻と現在時刻の差が5分以内なら許可」
理由:
攻撃者が署名付きリクエストを盗んでも、
5分後には無効になります。
例(Python)
import time
ALLOWED_DELAY = 300 # 5分
if abs(time.time() - int(timestamp)) > ALLOWED_DELAY:
abort(408) # Request Timeout
方法②:nonce(使い捨てトークン)を保存して再利用禁止
サーバー側が
「この nonce は今日初めて見たか?」
をチェックし、既に使われていれば拒否します。
例:Redis などを使うと簡単。
4. IP 制限は「補助」でしかない理由
IP 制限は有効な対策ですが、
単独では十分な対策ではありません。
大規模サービスは IP 範囲が変動する
CDN を使っている場合はエッジの IP になる
IP 偽装は難しいが、「正規サービスになりすました他サービス」から来る可能性はゼロではない
したがって、
HMAC(必須) + IP 制限(補助)
という形が最も安全です。
5. Webhook 設計で絶対やってはいけないこと
「URL を長くしておけば安全」は間違い
URL は秘密鍵ではありません。
HTTP を使う
通信内容が盗まれます。
署名を文字列比較(==)で判定する
タイミング攻撃のリスクがあります。
→ hmac.compare_digest() を使用する。
タイムスタンプを検証しない
リプレイ攻撃を防げなくなります。
8. まとめ
Webhook を安全に設計するには以下が必須です。
✔ HMAC 署名で送信元を検証
✔ タイムスタンプ・nonce でリプレイ防止
✔ HTTPS の利用
✔ IP 制限は“補助”として使う
✔ compare_digest を使う
✔ URL に頼ったセキュリティは危険
これらは「事実ベースの一般的なセキュリティ原則」で、
特定サービスに依存しない普遍的な内容です。
お読みいただきありがとうございました!!