このガイドのゴール

  • Webhook を受信するサーバサイドの実装に必要な要素を理解する
  • 署名検証(Signature Verification)で不正リクエストを排除する
  • 冪等性(Idempotency)で二重処理を防ぎ、非同期処理でタイムアウトを回避する

Webhook は「外部サービスから自分のサーバへのリアルタイム通知」です。ポーリングと違い、イベント発生時に即座にデータが届きます。ただし、受信側の実装が不十分だと、セキュリティホール・二重処理・データ欠損の原因になります。

🧩 このガイドで題材にする Webhook
それぞれ署名方式と配送特性が異なる
💳 決済通知
Stripe Webhooks
HMAC-SHA256 署名 + タイムスタンプ検証。リプレイ攻撃対策が組み込まれている。
🐙 コード変更通知
GitHub Webhooks
HMAC-SHA256 署名。push・PR・Issue 等のイベントごとにペイロード構造が異なる。
💬 メッセージ通知
Slack Incoming Webhooks
署名検証(Signing Secret)+ リクエストタイムスタンプの鮮度チェック。

Webhook 受信の全体フロー

🔧 Webhook 受信の処理フロー
検証 → 冪等性チェック → 非同期処理の 3 ステップ
外部サービス Stripe / GitHub POST 受信エンドポイント ① 署名検証 ② 冪等性チェック ③ 200 OK 返却 ジョブ投入 Queue Redis / SQS Worker: ビジネスロジック実行 403 拒否 署名不一致 → 即拒否 200 OK(処理スキップ) DB event_id 記録(冪等性)

署名検証の実装

Webhook の受信で最も重要なセキュリティ対策です。署名を検証しないと、攻撃者が偽の Webhook を送信してサービスを操作できます。

Stripe の署名検証

Stripe は Stripe-Signature ヘッダに HMAC-SHA256 署名とタイムスタンプを含めます。

// Laravel での Stripe Webhook 署名検証
use Stripe\Webhook;

Route::post('/webhooks/stripe', function (Request $request) {
    $payload = $request->getContent();
    $sigHeader = $request->header('Stripe-Signature');
    $endpointSecret = config('services.stripe.webhook_secret');

    try {
        $event = Webhook::constructEvent($payload, $sigHeader, $endpointSecret);
    } catch (\UnexpectedValueException $e) {
        return response('Invalid payload', 400);
    } catch (\Stripe\Exception\SignatureVerificationException $e) {
        return response('Invalid signature', 403);
    }

    // event を処理...
    return response('OK', 200);
});

GitHub の署名検証

GitHub は X-Hub-Signature-256 ヘッダに HMAC-SHA256 署名を含めます。

import hmac
import hashlib

def verify_github_signature(payload_body, signature_header, secret):
    expected = "sha256=" + hmac.new(
        secret.encode(), payload_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
🚨
署名検証をスキップしてはいけない

「開発中だから後で実装する」は事故の原因です。Webhook エンドポイントはインターネットに公開されるため、署名なしでは誰でも偽のイベントを送信できます。決済完了の偽通知で商品を無料で取得される、といった攻撃が実際に報告されています。

Slack の署名検証

Slack は X-Slack-SignatureX-Slack-Request-Timestamp を使います。タイムスタンプが 5 分以上古い場合はリプレイ攻撃として拒否します。

import time

def verify_slack_signature(body, timestamp, signature, signing_secret):
    # タイムスタンプの鮮度チェック(5分以内)
    if abs(time.time() - int(timestamp)) > 300:
        return False

    sig_basestring = f"v0:{timestamp}:{body}"
    expected = "v0=" + hmac.new(
        signing_secret.encode(), sig_basestring.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
💡
タイムスタンプ検証はリプレイ攻撃を防ぐ

署名だけでは、過去の正当な Webhook リクエストを記録して再送する「リプレイ攻撃」を防げません。Stripe と Slack はタイムスタンプ検証でこの攻撃に対応しています。タイムスタンプが古すぎるリクエストは拒否してください。

冪等性の確保

Webhook は配送保証のために同じイベントを複数回送信することがあります。二重処理を防ぐ仕組みが必要です。

実装パターン: イベント ID の記録

// Laravel での冪等性チェック
public function handleWebhook(Request $request)
{
    $eventId = $request->input('id'); // Stripe: evt_xxx, GitHub: delivery ID

    // 処理済みなら 200 を返して終了(再処理しない)
    if (WebhookEvent::where('event_id', $eventId)->exists()) {
        return response('Already processed', 200);
    }

    // イベントを記録(先に記録してから処理する)
    $record = WebhookEvent::create([
        'event_id' => $eventId,
        'event_type' => $request->input('type'),
        'payload' => $request->getContent(),
        'status' => 'processing',
    ]);

    // ジョブキューに投入
    ProcessWebhookJob::dispatch($record);

    return response('OK', 200);
}
⚠️
「処理してから記録」ではなく「記録してから処理」

処理中にサーバがクラッシュすると、イベントが記録されないまま次の配送で再処理されます。先に event_id を DB に記録し、処理完了後にステータスを更新する設計にしてください。

非同期処理の設計

Webhook の送信元は、レスポンスが返るまでの時間に制限を設けています。

サービス タイムアウト リトライ回数
Stripe 20 秒 最大 3 日間にわたり段階的にリトライ
GitHub 10 秒 失敗時は Recent Deliveries から手動で再送可能
Slack 3 秒 3 回(30 分・60 分・3 時間後)

Slack の 3 秒制限は特に厳しいため、受信したら即座に 200 を返し、処理はキューに投入するのが定石です。

// 即座に 200 を返し、処理はキューで行う
Route::post('/webhooks/slack', function (Request $request) {
    // 署名検証
    if (!$this->verifySlackSignature($request)) {
        return response('', 403);
    }

    // キューに投入して即 200 返却
    ProcessSlackEvent::dispatch($request->all());

    return response('', 200);
});

セキュリティのベストプラクティス

エンドポイントの保護

  • HTTPS 必須: HTTP のエンドポイントはペイロードが平文で流れる
  • 推測されにくい URL: /webhooks/stripe よりも /webhooks/stripe/{random_token} の方が安全
  • IP ホワイトリスト: Stripe・GitHub は送信元 IP を公開しているため、ファイアウォールで制限可能
  • 不要なイベントの無視: 処理しないイベントタイプは早期に return response('OK', 200) で抜ける

ログと監視

受信した Webhook はすべてログに記録し、以下の異常を監視してください。

  • 署名検証失敗の急増: 攻撃の可能性
  • 特定イベントの大量配送: 外部サービス側の障害か、自分の処理失敗による再送
  • 処理時間の増加: ビジネスロジックのボトルネック
📌
ローカル開発には ngrok や Stripe CLI を使う

Webhook はインターネットからアクセス可能なエンドポイントが必要です。ローカル開発では ngrok でトンネルを作るか、Stripe CLI の stripe listen --forward-to コマンドでローカルにフォワードできます。GitHub も gh webhook forward で同様のことが可能です。

本番チェックリスト

  • 署名検証 をすべての Webhook エンドポイントで実装している
  • タイムスタンプ検証 でリプレイ攻撃を防いでいる(Stripe / Slack)
  • 冪等性: event_id を DB に記録し、重複処理を防いでいる
  • 受信後 即座に 200 を返し、処理はキューで非同期実行している
  • エンドポイントは HTTPS で公開している
  • エラーログ に署名検証失敗・処理失敗を記録している
  • ローカル開発用の Webhook フォワーディング を設定している

まとめ

  • Webhook 受信の核心は 署名検証 → 冪等性チェック → 非同期処理 の 3 ステップ
  • 署名検証は セキュリティの最低ライン — スキップすると偽イベントで操作される
  • 冪等性は 「記録してから処理」 の順序が重要(クラッシュ耐性)
  • Slack は 3 秒、GitHub は 10 秒でタイムアウトするため キュー処理が必須
  • ログと監視で 署名失敗の急増・大量再送 を検知する体制を作る

この記事の情報は 2026 年 4 月時点のものです。各サービスの署名方式やリトライポリシーは変更される可能性があります。公式ドキュメントの Webhook セクションを併せて確認してください。