Webhook署名を検証する重要性
Marcelo Oliveira
2023年6月29日
0 分で読めますWebhookは、イベント通知などの情報をほぼリアルタイムで送受信するためのコールバック連携手法です。アプリケーションのイベントをきっかけにWebhookを実行し、HTTP経由で別のアプリケーションやサードパーティAPIにデータを送信できます。Webhook URLを設定し、外部のサービスと連携することで、ワークフローをカスタマイズ、拡張、変更できます。
Webhookには署名が付いている場合と付いていない場合があります。ただし、セキュリティ上のベストプラクティスとして、検証可能な署名を付けることをおすすめします。これにより、リクエストが想定どおりのWebhook送信元から届いたことを受信側で確認できます。
Webhookの現在の利用例をいくつかご紹介します。
Gitベースのバージョン管理プラットフォームで、開発者がリポジトリにコードをプッシュしたときに通知する。
メッセージングプラットフォームで、ユーザーがメッセージスレッドに返信したときに通知する。
小売店での購入に使用する決済方法が承認されたことを決済サービスから通知する。
Verizonのレポートによると、2022年に発生したシステム侵入インシデントのうち、サプライチェーン攻撃が62%を占めました。ソフトウェア開発ライフサイクル全体を通じて、開発とデプロイに関わるすべてのコンポーネントとプラクティスのセキュリティを維持することが重要です。脆弱性が悪用されると、関係するパートナーが侵害され、ソフトウェアサプライチェーン全体に支障をきたすおそれがあります。
こうした脅威に対処し、サプライチェーンのセキュリティを維持するため、サプライチェーン事業者はWebhook署名を適用しています。たとえばCircleCIでは、Webhook署名を使ってシークレットを検証し、悪意のある攻撃者ではなくCircleCIだけがWebhookを呼び出すようにできます。Travis CIもWebhookの検証プロセスで署名用HTTPヘッダーを提供しています。
このチュートリアルでは、リポジトリへのユーザーのコードプッシュを検知するGitHub WebhookをNode.jsで実装します。
前提条件
手順を進めるには、次の準備が必要です。
Node.jsとnpmをダウンロードしてインストールします。
ローカルのNode.jsアプリから公開URLへの安全なトンネルを開くリバースプロキシツール、ngrokをインストールします。
HTTPリクエストを作成し、結果を簡単に確認できるプラットフォーム、Postmanをインストールします。
Node.jsでWebhook署名を検証する
ローカルマシンに新しいフォルダーを作成し、次のコードを記述したwebhook.jsという新しいファイルを作成します。repo定数の値は、ローカルリポジトリのパスに置き換えてください。
次に、ターミナルで以下のコマンドを実行します。
webhook.jsファイルで実現しようとしていることは、次のとおりです。
repo定数には、監視対象のリポジトリが格納されているフォルダーのパスを設定します。createServer関数は、マシン上のポート8080でHTTPサーバーを起動します。Webhookアプリが起動し、http://localhost:8080エンドポイントでリクエストを受信できる状態になります。受信したリクエストを処理すると、body定数にリクエストの大部分が格納されます。エンドポイントへのリクエストは、GitHub WebhookからローカルのペイロードURL(http://localhost:8080)に通知が届いたことを意味します。この通知により、監視対象のリポジトリにユーザーがコードをプッシュしたことがわかります。
プッシュ先がmainブランチであることを確認します。そのために、リクエストのJSONデータをbodyオブジェクトに解析し、参照先がmainブランチと一致するかどうかを
isMain定数で示します。リクエストが許可された場合、ローカルリポジトリに移動するコマンドを実行します。最後に
git fetchコマンドとgit pullコマンドを実行して、リポジトリを更新します。
Postmanで悪意のあるリクエストをシミュレートする
ここでは、GitHubと連携するNode.jsアプリケーションを作成します。ただしGitHubを使う前に、Postmanで悪意のあるリクエストをシミュレートし、アプリケーションの脆弱性を確認します。これにより、署名が重要である理由を理解できます。
Postmanアプリを開き、ローカルのペイロードURL http://localhost:8080宛ての新しいHTTP POSTリクエストを設定します。
Bodyタブに、次のコードを追加します。

Sendをクリックします。Node.jsを実行しているターミナルを開き、何が起きたか確認してください。

ご覧のとおり、Node.jsアプリはGitHub以外の送信元からのリクエストも受け入れてしまいます。これは危険です。リクエスト元が正当な相手かどうかを検証できるよう、コードにWebhook署名を実装する必要があります。
ngrokを設定する
このシナリオでは、Node.jsアプリをローカルで実行しています。しかしGitHubからマシン上のhttp://localhost:8080エンドポイントにアクセスしてWebhookリクエストを送信することはできません。Node.jsアプリを公開サーバーに公開すれば解決できますが、開発マシン上で実行・デバッグするほうが、Webサーバーやクラウドにデプロイするより簡単です。そこでngrokを使います。ngrokは、ランダムな公開URLからローカルのWebhookエンドポイントへの安全なトンネルを開くリバースプロキシツールです。
マシンでターミナルを開き、次のngrokコマンドを実行してローカルのポート8080を公開します。
ngrokにより、安全なトンネルを通じてランダムな公開URLからローカルのポート8080にアクセスできるようになったことが確認できます。
次に、上記の公開ngrokサブドメインを使ってGitHub Webhookを設定します。
GitHub Webhookを設定する
GitHubの新しいリポジトリの作成ページにアクセスして、新しいリポジトリを追加し、Webhookを設定します。

上部のバーでSettingsをクリックします。次に、左側の一覧でWebhooksを選択し、Add webhookをクリックします。
![サイドバーで[Webhooks]が選択されたGitHubリポジトリの[Settings]ページ。[Webhooks]の見出しと[Add webhook]ボタンを指す矢印が表示されています。](https://res.cloudinary.com/snyk/image/upload/f_auto,w_2560,q_auto/v1688053591/blog-webhook-sig-settings-add-wh.jpg)
Add webhookフォームが開きます。ここで、開発者がリポジトリにコードをプッシュしたときに発生するイベントのWebhookを設定します。

次の手順でフォームに入力します。
Payload URLフィールドに、ローカルのNode.jsアプリケーションのエンドポイントに対応するngrokアドレスを入力します。
Content typeフィールドで「application/json」を選択し、GitHubからエンドポイントに送信されるコンテンツの種類を指定します。
Secretフィールドに「some-webhook-secret」と入力します。本番環境では、セキュリティを確保するため、より強力なパスワードやパスフレーズが必要です。
Which events would you like to trigger this webhookでJust the push eventを選択します。これにより、検証テストで実行できるリポジトリへのプッシュ操作に対して、GitHub Webhookが設定されます。
注:Webhookの設定では、SSL(Secure Socket Layer)検証を常に有効にすることをおすすめします。SSL検証を有効にすると、システム間で送信されるデータが安全に保護され、不正な第三者による傍受を防げます。
フォームへの入力が終わったら、Add webhookをクリックします。

Webhookを保護する
暗号化データを処理するために、Node.jsのcryptoモジュールを追加します。webhook.jsファイルの先頭に次の行を追加してください。
次に、GitHub Webhookで設定したものと同じシークレット文字列を保持する定数を宣言します。
注:シークレットをコードにハードコードすることは推奨されません。この方法はデモ目的に限ります。
req.on関数の冒頭で、HMACを使って計算したSHA256署名の値を保持する定数を宣言します。共有シークレットキーとGitHubのリクエストから渡される値(ここではchunkパラメーター)に対して暗号学的なSHA256ハッシュ関数を実行し、HMAC-SHA256署名を取得します。
次に、isAllowedイベント内でreq.on定数を宣言し、x-hub-signature-256ヘッダーが計算した署名と一致するかどうかを示します。crypto.timingSafeEqual関数を使って2つの変数を比較します。この関数は、攻撃者が値を推測する手がかりとなる処理時間の情報を漏らさずに比較できます。
上記のコードで一致が確認できれば、リクエストは悪意のある攻撃者ではなくGitHubから届いたと判断できます。リクエストがGitHubからのものでなければ、HTTP 401 Unauthorizedレスポンスを返します。
このコードに適用されているベストプラクティスをいくつかご紹介します。Webhookの開発時に取り入れてください。
文字列操作でWebhook署名を計算する際は、WebhookのペイロードにUnicode文字が含まれる場合があるため、ペイロードを常にUTF-8として処理してください。また、データの形式に要件がある場合は、受信データが想定どおりの形式であることを確認する検証方法を実装できます。
GitHubなど一部のWebhookプロバイダーは、リクエストに
x-hub-signatureヘッダーを含める場合があります。x-hub-signatureヘッダーはSHA-1を使って生成されます。SHA-1は脆弱なハッシュアルゴリズムと見なされていますが、後方互換性のためにのみ含まれています。可能な限り、強力なSHA-256アルゴリズムで生成されるx-hub-signature-256ヘッダーを使用してください。リクエストのペイロード本文のシークレットを使い、ハッシュ署名を
"sha256="で始めます。単純な等価演算子(
"=="または"===")ではなく、タイミング攻撃のリスクを低減する安全な比較方法を使います。現在のすべての言語やプラットフォームには、こうした安全な方法が少なくとも1つあります。このNode.jsプロジェクトでは、crypto.timingSafeEqual関数を使用しています。
webhook.jsの完全なコードはこちらから入手できます。
Webhookの保護について詳しくは、GitHubのドキュメントをご覧ください。
保護したWebhookエンドポイントをテストする
コードを変更したため、ローカルのNodeサーバーを再起動してください。コンソールでCtrl+C(MacではCommand+C)を押してNodeサーバーを停止し、再度node webhook.jsコマンドを実行します。
次にPostmanアプリに戻り、リクエストを再送信します。今度はNode.jsアプリからHTTP 401 Unauthorizedコードが返されます。

GitHubを開き、まだREADME.mdファイルがなければ追加し、すでにある場合は開きます。次のようにテキストを追加または変更してください。

最後に、Commit changesをクリックして、変更をリポジトリのmainブランチにプッシュします。

Node.jsのターミナルに戻り、リポジトリが正常にフェッチされ、プルされたことを確認します。

まとめ
Webhookを使うとツール間で通信でき、イベント発生時にリアルタイムのデータを受け取れます。システムにWebhookを統合すれば、柔軟で拡張性の高いコラボレーションワークフローを実現できます。定義済みのパラメーターを使うAPIでは、こうしたワークフローを実現できません。
Webhookは設計上公開されるため、セキュリティ上の脅威にさらされやすく、サプライチェーン攻撃を防ぐための適切な対策が欠かせません。
Webhook署名は推奨される解決策です。Webhookプロバイダーがシークレットを設定し、強力なハッシュアルゴリズムを使って署名を生成できるためです。署名は、Webhook間のデータ通信が検証済みの信頼できる相手との間でのみ行われることを確認する、シンプルで安全な方法です。
Capture the Flagを始めよう
オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。



