広告が嫌いですか? 行く 広告なし 今日

Webhook シグネチャ パラメータの検証を行い、偽装されたリクエストを停止する

更新日

HMAC-SHA256 Webhook シグネチャの仕組みについて学び、Python、Node.js、PHPでの定時実行検証の実装方法、および生産環境で静的失敗を引き起こす一般的な誤りについて説明します。

Webhook Signatures: Verify Payloads and Stop Spoofed Requests 1

誰でもPOSTリクエストをあなたのウェブ훅エンドポイントに送信できます。署名検証がなければ、あなたのサーバーはそのリクエストがストリップ、GitHub、またはあなたのインフラから来たものか、あるいは攻撃者が合法なイベントを再送しているかを知ることができません。

ウェブ훅署名検証がこれを解決します。支払い処理業者、バージョン制御プラットフォーム、電子商取引システムが、受信データに対して行動する前に信頼性を証明するために使用する方法です。この記事では、HMAC-SHA256署名がどのように機能するか、ステップバイステップの検証パターン、そして生産環境で失敗を引き起こす微妙な誤りについて説明します。

未認証ウェブ훅がセキュリティリスクである理由

ウェブ훅エンドポイントはインターネットに公開されたHTTPハンドラーです。検証がない場合、そのエンドポイントに到達したすべてのリクエストが処理されます。特に注目される2つの攻撃は以下の通りです:

  • 偽装(Spoofing) — 攻撃者が合法なイベント(支払い成功、サブスクリプション更新)に見せかけた偽のパラメータを構築し、実際の取引が発生しないまま、あなたのシステムにアクションをトリガーします。
  • リプレイ攻撃(Replay attacks) — 認証されたリクエストが送信中にキャプチャされ、後に再送されます。あなたのエンドポイントがイドンプト(idempotent)であっても保護されていない場合、同じイベントが複数回発火します。

これらの攻撃は、共有秘密鍵と暗号署名の組み合わせによって防止されます。送信者はパラメータを署名し、あなたが処理する前に署名を検証します。

HMAC-SHA256署名がどのように機能するか

HMAC(ハッシュベースメッセージ認証コード)は2つの入力を取ります: 秘密鍵 ファイル名パターン メッセージ。これらをSHA-256ハッシュ関数(ウェブ훅の実装ではほとんどがこれ)を通じて処理し、固定長の署名を出力します。

重要な性質:同じ秘密鍵とメッセージは常に同じ署名を生成し、メッセージ内の1つのバイトを変更すると、まったく異なる出力が得られます。秘密鍵を持たない誰もが、パラメータを完全に見ることができても、有効な署名を生成することはできません。

実際の流れは以下の通りです:

  1. あなたはサービス(例:ストリップ)にウェブ훅を登録します。サービスはあなたに 署名秘密鍵.
  2. を提供します。 HMAC-SHA256(secret, payload) サービスがイベントを送信するとき、
  3. を計算し、リクエストヘッダーに含まれます。
  4. あなたのサーバーがリクエストを受け取ると、保存された秘密鍵とリクエストボディの原始データを使って同じHMACを計算し、2つの署名を比較します。

一致する場合、リクエストは信頼できるものと見なされます。一致しない場合、リクエストを拒否します。

以下のPython実装は、主要なサービスが期待するパターンを再現しています:

import hmac
import hashlib

def verify_signature(payload: bytes, secret: str, received_sig: str) -> bool:
    expected = hmac.new(
        key=secret.encode("utf-8"),
        msg=payload,
        digestmod=hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, received_sig)

HMAC呼び出し自体に加えて、2つの点が重要です。まず: payloadbytes、デコードされた文字列ではなく — あなたはネットワークから正確に受け取った 原始リクエストボディ を渡す必要があります。第二に、比較は hmac.compare_digest の代わりに ==を使用します。これは意図されています。

定時比較がオプションではない理由

多くの言語での文字列比較は短絡的に動作します:不一致の文字を見つけた時点で false を返します。攻撃者が応答時間を測定できる場合、これを利用し、異なる署名を数千回送信して、1文字ずつ正確な値を推測することができます。これは タイミング攻撃.

hmac.compare_digest Pythonで hash_equals PHPで crypto.timingSafeEqual Node.jsですべて、文字列がどこで異なるかに関わらず、同じ時間が必要です。すべての署名比較で、常に定時比較を使用してください。

実際のヘッダー形式:ストリップ、GitHub、Shopify

各サービスはわずかなヘッダー名や署名エンコーディングの違いを持っています。以下の3つの一般的な統合に必要なものを解析します。

Stripe

ストリップは Stripe-Signature ヘッダーを送信し、このフォーマットを持っています:

Stripe-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a05bd539e6d5b9d2a2d2fbe

t フィールドはイベントが送信されたUnixタイムスタンプです。値は v1 のHMAC-SHA256です。手動で実装する場合: timestamp + "." + raw_bodyヘッダーから

  1. を解析し、 tv1.
  2. を計算し、 HMAC-SHA256(secret, t + "." + raw_body).
  3. と定時比較を行います。 v1 現在時刻から300秒(5分)を超える場合、拒否します。
  4. GitHubは t を削除し、残りの部分をあなたの原始ボディのHMACと比較します。GitHubは署名にタイムスタンプを埋め込んでいないため、リプレイ保護が必要な場合は、イベントIDを別途デュピリケート処理します。

GitHub

Shopifyは X-Hub-Signature-256 ヘッダーに配置します:

X-Hub-Signature-256: sha256=6ffbb59b2300aae63f272406069a9788598b770f698a48021f99b32f8de06bb3

を使用し、デジットをBase64エンコードしています: sha256= あなたの計算されたHMACとヘッダー値をBase64から原始バイトにデコードし、定時関数で比較します。Base64文字列を直接比較すると、微妙なエンコーディング標準化のバグが生じる可能性があります。

Shopify

最も一般的な誤り:原始ボディとパースされたJSON X-Shopify-Hmac-Sha256 これは、生産環境でウェブ훅署名検証が失敗する最も一般的な原因です。あなたのフレームワークがハンドラーが実行される前にリクエストボディをdictやオブジェクトに自動パースする場合、原始バイトは失われます。HMACはその原始バイトに対して計算されたものであり、パースされた構造を再エンコードしたものに対して計算されたものではありません。

X-Shopify-Hmac-Sha256: b6LPiZidmXnJQf0Ff/p7MZQPIBPN9TqAqLMgAfn2YLQ=

データが意味的に同じであっても、

(コロンの後にスペースがない)は異なるHMAC値を生成します。フィールドの順序、Unicodeの標準化、浮動小数点精度の違いがすべて、明確なエラーなしで検証を破壊する可能性があります。

解決策:

パース前にリクエストボディをバッファリングし、そのバイトを検証関数に渡します。Express.jsでは、ルートを {"amount": 100}{"amount":100} ミドルウェアで登録し、

(バイト)を使用します。Laravelでは を使用します。パターンは一貫しています:リクエストから原始バイトに直接アクセスし、パースされた表現を再シリアル化しないでください。 express.raw() タイムスタンプによるリプレイ保護 express.json():

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body; // Buffer — not a parsed object
  const sig = req.headers['stripe-signature'];
  // Verify rawBody against sig before JSON.parse()
});

Djangoでは request.body 有効な署名は、パラメータが送信中に改ざんされたかどうかを証明するだけです。リプレイ攻撃を防ぐことはできません。合法なリクエストがキャプチャされ、数時間後に再送された場合、署名は依然として検証され、何も変化していないため、署名が正しく検証されます。 $request->getContent()ストリップは、タイムスタンプを署名されたパラメータに含め、5分の許容範囲を推奨しています。ヘッダーから

を抽出し、タイムスタンプがその範囲外にある場合を拒否します:

GitHubやShopifyのような、署名にタイムスタンプを埋め込んでいないサービスでは、イベントID(パラメータに含まれている)をストレージし、すでに処理したIDを拒否することで、自らのリプレイ保護を実装します。短い有効期間を持つキャッシュまたはRedisセットを使用すると、処理範囲に合った結果が得られます。

コードを書かずに署名を検証する t ウェブ훅統合のデバッグや、既に受け取ったパラメータを検証する場合、 Stripe-Signature にパラメータ、秘密鍵、受け取った署名を貼り付けて、ローカル環境を起動せずに即座に検証できます。

import time

def is_within_tolerance(timestamp: int, tolerance_seconds: int = 300) -> bool:
    return abs(time.time() - timestamp) <= tolerance_seconds

# In your handler:
if not is_within_tolerance(stripe_timestamp):
    return HttpResponse(status=403)  # Reject stale request

テスト用にエンドポイントにカスタムパラメータを送信する場合、

を使用します。これはSHA-256、SHA-512、および他のいくつかのハッシュアルゴリズムのヘキサデシマルおよびBase64出力を生成し、各サービスが使用するフォーマットをカバーするテストケースを作成するために役立ちます。

ストリップ、GitHub、Shopify、および他の多くのサービスで標準的な署名スキームはHMAC-SHA256です。 Webhook署名バリデーター 常に定時比較(

)を使用してください — ではなく HMAC ジェネレーター原始リクエストボディバイトをHMAC関数に渡し、再シリアル化されたJSONオブジェクトを渡さないでください。

速携帯用参考

  • タイムスタンプを含むサービスについては、タイムスタンプをチェックしてください。ストリップの許容範囲は5分です。
  • タイムスタンプを含まないサービスについては、イベントIDで重複を防ぎます。hmac.compare_digest, hash_equals, crypto.timingSafeEqual各サービスのヘッダー名およびエンコーディング(ヘキサデシマルとBase64)は異なります — 常に統合ドキュメントを確認してください。 ==.
  • ウェブ훅署名:パラメータを検証し、偽のリクエストを停止する 2
  • ウェブ훅署名:パラメータを検証し、偽のリクエストを停止する 1
  • タイムスタンプベースのリプレイ保護がないサービスについては、イベントIDで重複を削除する。
  • ヘッダー名およびエンコーディング(ヘキサデシマル対Base64)はサービスごとに異なります — 常に統合ドキュメントを確認してください。
広告なしで楽しみたいですか? 今すぐ広告なしで

拡張機能をインストールする

お気に入りのブラウザにIOツールを追加して、すぐにアクセスし、検索を高速化します。

に追加 Chrome拡張機能 に追加 エッジ拡張 に追加 Firefox 拡張機能 に追加 Opera 拡張機能

スコアボードが到着しました!

スコアボード ゲームを追跡する楽しい方法です。すべてのデータはブラウザに保存されます。さらに多くの機能がまもなく登場します!

ニュースコーナー 技術ハイライト付き

参加する

価値ある無料ツールの提供を継続するためにご協力ください

コーヒーを買って