InstaTunnel
Navigation menu o kirikae
API
13 min read
40 views

署名済み、封印済み、ぐちゃぐちゃに: なぜウェブフック署名はトンネルの背後で失敗するのか

トンネルの背後でウェブフック署名が失敗していますか? 生の本文、JSON の再シリアライズ、タイムスタンプの許容範囲、そして Twilio の URL 署名が HMAC チェックを壊します。対処法は以下のとおりです。

IT
InstaTunnel Team
Published by the InstaTunnel team | Editorial policy
署名済み、封印済み、ぐちゃぐちゃに: なぜウェブフック署名はトンネルの背後で失敗するのか

Quick answer

トンネルの背後で失敗するウェブフック署名?対処法はこちら: quick comparison answer

Choose the tunnel tool based on the network model: public HTTPS URLs for webhooks and demos, private mesh access for internal apps, and managed infrastructure when policy controls matter most.

Which tunnel tool is best for public webhook testing?

Use a public HTTPS localhost tunnel with stable URLs. InstaTunnel focuses on webhook testing, demos, OAuth callbacks, and MCP endpoint workflows.

When should I choose a private network tool instead?

Choose a private mesh or Zero Trust tool when every user and service should stay inside a controlled private network.

あなたはトンネルを localhost:3000 に向け、テストイベントを送信し、ハンドラは 400 Invalid signature と返します。ペイロードは正しく見え、シークレットも正しいようです。では、なぜ ウェブフック署名検証 はリクエストがトンネルやリバースプロキシを通過した場合だけ失敗するのでしょうか?

署名は、厳密なバイト列と厳密なタイミング、そして一部の提供者にとっては厳密な URL を約束するものです。トンネル、プロキシ、または便利なミドルウェアは、それらのいずれかを変更しても、リクエスト自体は健全に見えるままです。本ガイドでは、開発者が最も直面する3つの失敗のパターンを取り上げます:生の本文と再シリアライズ済み JSON の違い、タイムスタンプの許容範囲、そして Twilio 風の全 URL 署名。最後にデバッグのチェックリストと、2026年11月時点で利用可能なデバッグツールの紹介をします。

ウェブフック署名の仕組み

ほぼすべての提供者は HMAC を使用します。送信者は共有シークレットと“署名済みコンテンツ”を組み合わせ、それをハッシュして結果をヘッダに入れます。受信者は同じコンテンツを再構築し、自分の HMAC を計算して、2つを一定時間で比較します。署名内容に入るものやハッシュアルゴリズム、エンコードはプロバイダごとに異なります。

提供者 ヘッダ 署名内容 アルゴリズム
Stripe Stripe-Signature (t=…,v1=…) {timestamp}.{raw body} HMAC-SHA256, hex
GitHub X-Hub-Signature-256 (sha256=…) raw payload only HMAC-SHA256, hex
Slack X-Slack-Signature (v0=…) v0:{timestamp}:{raw body} HMAC-SHA256, hex
Standard Webhooks (Svix, Resend, Clerk, Polar, OpenAI and others) webhook-signature (v1,…) {webhook-id}.{webhook-timestamp}.{raw body} HMAC-SHA256, Base64
Twilio X-Twilio-Signature full URL + sorted POST params HMAC-SHA1, Base64

Stripe, Slack and Standard Webhooks は タイムスタンプ を署名に組み込みます。GitHub はタイムスタンプを含めません。Twilio は URL を署名します。それぞれの設計は、トンネルにとって物事を壊す別の方法を与えます。

失敗 #1: 生の本文と再シリアライズ済み JSON

“署名ミスマッチ”の最も一般的な原因は、間違ったシークレットではありません。検証コードの前に実行される JSON パーサーです。

Stripe は未変更の生のリクエスト本文に署名します。ほとんどのウェブフレームワークは本文を解析して元のバイト列を破棄します。Slack のドキュメントも同じ点を指摘しています。本文の生の形式(空白を含む)が、Slack が署名を計算する際に使用するものです。

「ぐちゃぐちゃ」とはどういう見た目です

JSON を解析してから JSON.stringify(req.body) を呼び出して HMAC に渡すと、プロバイダが署名した文字列とは異なる文字列を検証していることになります。再シリアライズは次のを変えることがあります:

  • 空白文字とキーの順序、元のバイト列と1対1で一致することはめったにありません。
  • Unicode のエスケープ:送信者は "\\u00e9" のように書くことがあり、あなたのシリアライザはリテラルな "é" を書くことがあります。どちらも同じ値ですがバイト列が異なります。
  • 数値のフォーマット:JavaScript では、1.0 は 1 になり、2^53 を超える整数は精度を失います。
  • 文字エンコーディング:GitHub の指針は、ペイロードを UTF-8 として扱うことです。 Unicode を含むことがあるためです。

これらはいずれも、有効そうに見えるオブジェクトを生み出しますが、署名は無効になります。

Express の落とし穴

Express では、ミドルウェアの順序がすべてを決定します。どのボディパーサーが先に実行されるかで、リクエストストリームを消費してしまいます。これは壊れています:

app.use(express.json());                                   // runs first; req.body is now an object
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), handler);

ルートレベルの raw パーサーは何も取得できません。Webhook のルートは express.raw() をグローバル JSON パーサーよりも前に登録するか、verify フックを使ってバイト列を取得してください:

app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; }   // keep the exact bytes
}));

req.rawBody をハッシュし、決して JSON.stringify(req.body) で検証しないこと。生データルートの Buffer.isBuffer(req.body) ガードは、紛らわしい“署名ミスマッチ”を明らかな設定エラーに変えます。

トンネルが介在するとき

HTTP リクエストのみを中継するトンネルは、受信したときと同じボディのバイト列を返すべきです。問題は、経路上の何かがリクエストを変換する場合に発生します。本文を書き換えるプロキシ、本文を正規化するミドルウェア、再エンコードする検査レイヤーなどです。トンネルを通ると検証に失敗する場合は、アプリが受信する生のバイト列と、プロバイダが送信したものを比較してください。

トンネル検査はここでも役立ちますが、1点だけ注意があります。例えば ngrok の Traffic Inspector は、機密情報の露出を避けるためデフォルトでリクエストのメタデータのみを収集します。完全なリクエストとレスポンスの本文を記録する前に、アカウント設定でフルキャプチャを有効にしてください。

さらにもう一つのローカル開発時の罠: 署名秘密はイベントの発生元に依存します。 Stripe のトラブルシューティングガイドは、最も一般的な検証エラーは間違ったエンドポイント秘密を使うことだと述べています。ダッシュボードのエンドポイントと Stripe CLI はともに whsec_ で始まる秘密を生成しますが、それぞれ別の秘密です。CLI 経由のイベントをダッシュボードのエンドポイントの秘密で検証したり、その逆を行ったりしないでください。どんなに本文を処理しても、それを修正することはできません。

失敗 #2: タイムスタンプの許容範囲

タイムスタンプ付き署名はリプレイ攻撃を防ぐために存在します。有効なリクエストを傍受した攻撃者が署名をそのまま再送信できるため、受信側はタイムスタンプが新しいかどうかも確認します。一般的なデフォルトは5分です:

  • Stripe のライブラリは、タイムスタンプがデフォルトで300秒以上ずれているイベントを拒否します。許容範囲を上書きすることもできます。
  • Slack は、X-Slack-Request-Timestamp がローカル時刻と5分以上異なる場合、リクエストを拒否するように指示します。
  • Standard Webhooks ライブラリはデフォルトで300秒の許容範囲を設定しています。実装によっては両方向をチェックし、将来のタイムスタンプや古いタイムスタンプの両方を拒否します。

Standard Webhooks では、webhook-id は同じイベントの再試行を跨いでも同じままですが、webhook-timestamp は各配信試行が署名された時刻を記録します。したがってタイムスタンプは、プロバイダ側の再試行のたびに新鮮です。

GitHub は例外です。 署名にはタイムスタンプが含まれていないため、新鮮性チェックはありません。代わりに X-GitHub-Delivery の ID で重複を排除します。

なぜトンネルはタイムスタンプの失敗を起こしやすくするのか

  1. 開発機の時計のずれ。 一晩眠っていたノートPCや、時計がずれた VM やコンテナは数分ずれることがあります。許容範囲を超えると、署名が正しくてもすべてのリクエストが失敗します。まず時計を同期してください。
  2. キャプチャしたイベントのリプレイ。 トンネル検査は、キャプチャしたリクエストを編集を含めて再送信することを許します。ngrok は通常のリプレイと「変更付きリプレイ」の両方を提供します。1時間以上前の Stripe イベントは許容範囲チェックに失敗しますが、これは設計どおりの動作です。新しいタイムスタンプで再署名するか、開発時のみ許容範囲を広げてください。
  3. 転送中の遅延。 プロバイダの再試行は再署名されるため、それ自体が原因ではありません。トンネルが配信を落とす、キューに入れる、もしくは許容範囲のウィンドウを超えて遅延させるとき、それが原因になります。

本番環境では、300秒のデフォルトを維持し、使い捨てのテスト以外でこの検証を無効化しないでください。拒否のたびに計算された時計のずれをログに残し、「stale timestamp(古いタイムスタンプ)」と「bad signature(不正署名)」を別々のエラーとして表示します。Slack の公式指示は、署名比較の前の別個の手順としてタイムスタンプ検査を扱います。

A Standard Webhooks gotcha

Standard Webhooks の署名キーは、通常は whsec_ のプレフィックスを削除し、残りを Base64 デコードした秘密です。すべてのプロバイダが厳密にそれに従うわけではありません。Polar のドキュメントは、Polar がプレフィックスを含む完全な秘密文字列を生の HMAC キーとして使用することを指摘しています。ライブラリを使わず自作で検証する場合は、プロバイダのドキュメントを確認してください。

失敗 #3: Twilio は全 URL に署名する

Twilio のスキームは人々を最も驚かせます。本文だけでなく、署名対象になるのは URL の一部でもあるからです。Twilio のドキュメントによれば:

  1. リクエストの完全な URLを、プロトコルからクエリ文字列の末尾まで含めて作成します。
  2. フォームエンコードされた POST の場合、POST パラメータを名前のアルファベット順にソートし、各名前と値を区切り文字なしで URL に追加します。
  3. その文字列の HMAC-SHA1 を、あなたのアカウントの Auth Token を鍵として計算します。
  4. 結果を Base64 エンコードし、X-Twilio-Signature と照合します。

URL が署名に組み込まれているため、スキーム、ホスト、パスを変更するプロキシは検証を壊します。署名は正当ですが、Twilio がハッシュしたものと異なる文字列を再構築します。

Twilio の検証ヘルパーは、Twilio がいつでもパラメータを追加できるため、受信したすべてのパラメータをハードコードされたリストではなく渡すことを期待します。クエリ文字列のパラメータはすでに URL の一部なので、それらを別々に渡さないでください。

トンネルの背後で URL を壊すもの

Twilio のサポートは、プロキシの背後で 403 が発生する根本原因は、Twilio が署名した URL とアプリが再構築する URL の不一致がほとんどだと述べています。よくある原因は以下のとおりです:

  • 間違ったスキーム。 TLS はエッジで終わるため、あなたのアプリは http:// を見る一方、Twilio は https:// を署名しました。元のスキームを X-Forwarded-Proto で復元します。Twilio の Node.js SSL termination ガイダンスもこれを説明しています。
  • 間違ったホスト。 アプリは内部ホスト名や localhost:3000 を見ますが、Twilio は公開トンネルのホスト名を署名しました。パスのいずれかが Host ヘッダを書き換えている場合、それを用いて署名済み URL を再構築しないでください。
  • パスやクエリ文字列が欠如。 request.url や request.host のようなフレームワークのオブジェクトは部分的な URL を返すことがあり、パスを含まない https://host を再構築すると署名文字列が変わります。
  • 末尾のスラッシュ。 署名された値はプレーンな文字列なので、/webhook と /webhook/ は異なる署名を生み出します。Twilio Console 上の URL と正確に一致させてください。
  • ポート。 twilio-ruby の検証器は、明示的なポートの有無の両方で署名を検証します。Twilio のバックエンドの署名生成が一貫していないことを示すコードコメントがあります。独自の検証を行う場合は、両方の形式をテストしてください。
  • 変更するホスト名。 セッションごとに新しいランダムなホスト名が割り当てられるトンネルの場合、Twilio と検証器の URL を更新してください。古い値はすべてのリクエストで失敗します。

JSON ボディと bodySHA256

すべての Twilio Webhook がフォームエンコードされているわけではありません。application/json のリクエストでは、Twilio は webhook URL に bodySHA256 というクエリパラメータを付与し、生の本文の SHA-256 ハッシュを保持します。Twilio の SDK は次の2つのチェックを実行します:本文のハッシュがパラメータと一致すること、そして URL をこのパラメータを含めたものとして評価した署名が有効であること。本文を改ざんするミドルウェアは、URL ロジックが完璧でもハッシュチェックを失敗させます。Node ではこれが validateRequestWithBody の仕事です。ここでも生の本文を保持してください。

最も安全な対処法

  1. 自分が管理するプロキシからのみ転送ヘッダを信頼してください。 X-Forwarded-Proto と X-Forwarded-Host を使って URL を再構成します。これは信頼できるプロキシが実際にそれらを設定している場合に限り機能します。Express では、trust proxy を正しく設定して req.protocol が転送スキームを反映するようにします。
  2. 公開URLをハードコードします。 すでに Twilio で設定したものを検証します。常に同じ URL を使い、再構成のバグを排除します。

Twilio のスキームにはタイムスタンプはないため、リプレイウィンドウはありません。リプレイが必要な場合は、識別子(たとえば MessageSid や CallSid)で重複を除外してください。

10 分のデバッグチェックリスト

安価なチェックから高価なチェックへ順に行います:

  1. 正しい秘密、正しい種類? Stripe CLI とダッシュボードの秘密、Twilio の Auth Token(署名キー)と API キー、伝播していない rotationSecret。GitHub は X-Hub-Signature-256 を省略します。
  2. 正しいヘッダ? GitHub は X-Hub-Signature-256 を推奨します。古い X-Hub-Signature は SHA-1 を使い、レガシー用途のために残されています。Stripe のテストイベントには古い v0 エントリがヘッダに含まれることがあるため、v1 を検証し、他のスキームを無視してください。
  3. 生のバイト列? ハッシュする本文の長さをログに出力し、トンネルが見た Content-Length と比較してください。
  4. 時計は許容範囲内? 拒否時に now - timestamp を出力してください。
  5. Twilio: どの URL をハッシュしていますか? それをログに記録し、Console と1文字ずつ比較してください。ポートの有無と末尾のスラッシュの有無で試してください。
  6. アプリへ直接送信して通るか? はいなら、プロバイダとアプリの間でリクエストが変更されています。いいえなら、検証コードのバグです。
  7. 一定時間比較? crypto.timingSafeEqual や hmac.compare_digest を使います。それは不一致を修正するわけではありませんが、GitHub のドキュメントは推奨しており、デバッグ時のタイミングの漏れを防ぎます。

2026 年後半に知っておくべきツール

  • Stripe CLI. stripe listen はイベントを localhost にフォワードし、独自の一時署名秘密を表示します。公開URLは不要です。ローカルでその秘密を使ってください。
  • ngrok. その Traffic Inspector はキャプチャしたリクエストを表示・リプレイでき、提供者検証機能はサポートされているプロバイダの webhook 署名をチェックして、アプリに到達する前に不正なリクエストをブロックします。ngrok の Mux ガイドは無料アカウントで月500件の検証の上限を示しています。
  • @teamshift/webhook-inspect. 最近のオープンソース npm パッケージで、Stripe、GitHub、Shopify、Slack、Twilio などの HMAC 署名を検証し、なぜ検証が失敗しているかを説明します。その README には 3 つのコマンドが記載されています:
    • verify は 1 件のリクエストを検証します。
    • listen は Webhooks をローカルで捕捉し、検証します。
    • replay は保存したキャプチャをローカルサーバーへ送信します。

webhook-inspect の注意点: Socket のリストには 0.1.0 のような単一のバージョンと 1 人のメンテナーが表示されます。README には 2026 年 10 月 4 日付のサンプルキャプチャがありますが、正確な公開日を確認できませんでした。初期段階のソフトウェアとして扱い、実際の機密情報を通す前に検討してください。

どのツールを使っても、手法は同じです。提供者の署名文字列を再現し、変数を 1 つずつ変更して HMAC が一致するまで試してください。

本番環境の強化

  • パース前に検証。 認証を最初に、デシリアライズはその次に行います。
  • 欠落した内部エラー(ヘッダ不足、タイムスタンプの古さ、不正署名)を分離し、呼び出し元には一般的な応答を返します。
  • 秘密の回転をサポート。 Stripe は複数の v1 署名を受け付け、Standard Webhooks はスペース区切りの署名を複数持てます。どれか正しいものを受け付けてください。
  • 開発時には安定した公開 URL を使用。 Twilio 風の URL 署名には、セッション間で変わらないトンネルのホスト名を使えば、プロバイダのコンソールと検証機の同期が保たれます。
  • 検証をスキップするフラグを出荷時に有効のままにしない。

よくある質問

なぜ私の署名はローカルでは検証できるのに、トンネルの背後では失敗しますか?

通常、本文のバイト列(パーサーやプロキシが変更した)、時計のずれ(リプレイされたイベント)または URL(署名されたものと異なるスキーム、ホスト、パス)のどれかが原因です。特に Stripe CLI と Dashboard の秘密の違いを検証対象から外してください。生のバイト列、ずれ、再構成した URL の順に確認します。

Twilio の署名にはタイムスタンプが含まれますか?

いいえ。Twilio は完全な URL とソートされた POST パラメータを署名します(bodySHA256 を使用する場合もある)。Stripe、Slack、Standard Webhooks はタイムスタンプを含みます。

適切なタイムスタンプの許容範囲は?

5 分(300 秒)は Stripe のライブラリと Standard Webhooks のライブラリで広く使われるデフォルトで、Slack のドキュメントが示すウィンドウです。ローカルのテストでのみ拡大してください。

JSON を解析した後に署名を検証できますか?

信頼性は低いです。再シリアライズされた JSON は空白、キー順序、エスケープ、数値のフォーマットが異なることがあります。パーサが走る前の生のバイト列をキャプチャしてください。

ホスト名が正しく見えても、Twilio の検証はなぜ失敗しますか?

スキーム、ポート、パス、クエリ文字列、末尾のスラッシュを確認してください。1 文字の違いが HMAC を変えます。迷ったら Twilio Console に表示されている URL をハードコードしてください。

結論

ウェブフック署名は小さな契約です:*この正確なバイト列をこの時点でこのアドレスに**。トンネルやプロキシは悪意ではありませんが、その契約の途中に介在し、条項のどれかを乱す可能性があります。生の本文を保ち、時計を正直に保ち、正確な URL を再構成するかハードコードし、ログを見て解決できない場合はリプレイと診断ツールを使ってください。そうすれば“signed, sealed, mangled” は再び“signed, sealed, delivered” になります。


出典

Continue from this article into the most relevant product guides and workflows.

Related Topics

#webhook signature verification#webhook signature failed#webhook signature mismatch#webhook signature debugging#webhook signature behind tunnel#webhook tunnel#localhost tunnel webhooks#ngrok webhook signature#ngrok webhook signature invalid#cloudflare tunnel webhooks#localtunnel webhooks#tunneling tools#webhook testing localhost#local webhook development#webhook debugging#webhook inspector#webhook security#HMAC signature verification#HMAC SHA256 webhook#raw body webhook verification#raw body vs parsed JSON#JSON re-serialization signature#express raw body middleware#body parser signature verification#webhook timestamp tolerance#webhook replay attack prevention#replay protection#clock skew webhook#Twilio signature validation#Twilio request validator#Twilio X-Twilio-Signature#Twilio webhook URL signing#Twilio webhook invalid signature#reverse proxy webhook#proxy rewrites host header#X-Forwarded-Host webhook#X-Forwarded-Proto signature#Host header rewrite#webhook URL mismatch#HTTPS termination webhook#Stripe webhook signature#Stripe webhook raw body#GitHub webhook signature#Shopify webhook HMAC#Slack signing secret#Slack request signature#webhook secret key#constant-time comparison#timing attack webhook#signature verification error#401 invalid signature webhook#webhook best practices#webhook troubleshooting#developer infrastructure#DevOps networking#API security#cloud-native development#HTTP request signing#webhook middleware#October 2026 webhook tools

Keep building with InstaTunnel

Read the docs for implementation details or compare plans before you ship.

Share this article

More InstaTunnel Insights

Discover more tutorials, tips, and updates to help you build better with localhost tunneling.

Browse All Articles