Skip to main content

Webhook署名を検証する重要性

著者

Marcelo Oliveira

blog feature playwright tests

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定数の値は、ローカルリポジトリのパスに置き換えてください。

const http = require('http');
const exec = require('child_process').exec;

const repo = "C:\\Users\\your-user-name\\Documents\\GitHub\\webhook-test";

http
  .createServer((req, res) => {
    req.on('data', chunk => {

      const body = JSON.parse(chunk);

      console.log(body);

      const isMain = body?.ref === 'refs/heads/main';

      if (isMain) {
        try {
            console.log('Push event detected. Pulling repository updates...');
            exec(`cd ${repo} && git fetch && git pull`);
            console.log('Git repository pulled successfully.');
        } catch (error) {
          console.log(error);
        }
      }
      res.end();
    });
  })
  .listen(8080);

次に、ターミナルで以下のコマンドを実行します。

node webhook.js

webhook.jsファイルで実現しようとしていることは、次のとおりです。

  1. repo定数には、監視対象のリポジトリが格納されているフォルダーのパスを設定します。

  2. createServer関数は、マシン上のポート8080でHTTPサーバーを起動します。Webhookアプリが起動し、http://localhost:8080エンドポイントでリクエストを受信できる状態になります。

  3. 受信したリクエストを処理すると、body定数にリクエストの大部分が格納されます。エンドポイントへのリクエストは、GitHub WebhookからローカルのペイロードURL(http://localhost:8080)に通知が届いたことを意味します。この通知により、監視対象のリポジトリにユーザーがコードをプッシュしたことがわかります。

  4. プッシュ先がmainブランチであることを確認します。そのために、リクエストのJSONデータをbodyオブジェクトに解析し、参照先がmainブランチと一致するかどうかをisMain定数で示します。

  5. リクエストが許可された場合、ローカルリポジトリに移動するコマンドを実行します。最後にgit fetchコマンドとgit pullコマンドを実行して、リポジトリを更新します。

Postmanで悪意のあるリクエストをシミュレートする

ここでは、GitHubと連携するNode.jsアプリケーションを作成します。ただしGitHubを使う前に、Postmanで悪意のあるリクエストをシミュレートし、アプリケーションの脆弱性を確認します。これにより、署名が重要である理由を理解できます。

Postmanアプリを開き、ローカルのペイロードURL http://localhost:8080宛ての新しいHTTP POSTリクエストを設定します。

Bodyタブに、次のコードを追加します。

{
    "ref": "refs/heads/main",
    "comment": "hi, I'm a malicious request..."
}
webhookのペイロードと、webhookの署名がペイロードのハッシュと一致しないことを示す401 Unauthorizedのレスポンスが表示されたPostmanリクエスト。

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

Git pushイベントの検出、リポジトリの更新の取得、リポジトリの取得成功を示すターミナル出力。

ご覧のとおり、Node.jsアプリはGitHub以外の送信元からのリクエストも受け入れてしまいます。これは危険です。リクエスト元が正当な相手かどうかを検証できるよう、コードにWebhook署名を実装する必要があります。

ngrokを設定する

このシナリオでは、Node.jsアプリをローカルで実行しています。しかしGitHubからマシン上のhttp://localhost:8080エンドポイントにアクセスしてWebhookリクエストを送信することはできません。Node.jsアプリを公開サーバーに公開すれば解決できますが、開発マシン上で実行・デバッグするほうが、Webサーバーやクラウドにデプロイするより簡単です。そこでngrokを使います。ngrokは、ランダムな公開URLからローカルのWebhookエンドポイントへの安全なトンネルを開くリバースプロキシツールです。

マシンでターミナルを開き、次のngrokコマンドを実行してローカルのポート8080を公開します。

ngrok http 8080

ngrokにより、安全なトンネルを通じてランダムな公開URLからローカルのポート8080にアクセスできるようになったことが確認できます。

Forwarding https://<<YOUR-RANDOM-SUBDOMAIN>>.sa.ngrok.io -> http://localhost:8080

次に、上記の公開ngrokサブドメインを使ってGitHub Webhookを設定します。

GitHub Webhookを設定する

GitHubの新しいリポジトリの作成ページにアクセスして、新しいリポジトリを追加し、Webhookを設定します。

「新しいリポジトリを作成」というタイトル、非表示のオーナー名、リポジトリ名「webhook-test」が表示されたリポジトリ作成フォーム。

上部のバーでSettingsをクリックします。次に、左側の一覧でWebhooksを選択し、Add webhookをクリックします。

サイドバーで[Webhooks]が選択されたGitHubリポジトリの[Settings]ページ。[Webhooks]の見出しと[Add webhook]ボタンを指す矢印が表示されています。

Add webhookフォームが開きます。ここで、開発者がリポジトリにコードをプッシュしたときに発生するイベントのWebhookを設定します。

ペイロードURL、JSONコンテンツタイプ、シークレット欄、イベントのトリガーオプション、「Active」チェックボックスが表示された、GitHubの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をクリックします。

GitHubのWebhook追加フォーム。ペイロードURL、JSONコンテンツタイプ、シークレット、プッシュイベントの選択箇所を矢印で示しています。

Webhookを保護する

暗号化データを処理するために、Node.jsのcryptoモジュールを追加します。webhook.jsファイルの先頭に次の行を追加してください。

const crypto = require('crypto');

次に、GitHub Webhookで設定したものと同じシークレット文字列を保持する定数を宣言します。

const secret = 'some-webhook-secret';

注:シークレットをコードにハードコードすることは推奨されません。この方法はデモ目的に限ります。

req.on関数の冒頭で、HMACを使って計算したSHA256署名の値を保持する定数を宣言します。共有シークレットキーとGitHubのリクエストから渡される値(ここではchunkパラメーター)に対して暗号学的なSHA256ハッシュ関数を実行し、HMAC-SHA256署名を取得します。

    req.on('data', chunk => {

      const hashAlgorithm = 'sha256'
      const signature = Buffer.from(req.headers['x-hub-signature-256'] || '', 'utf8')
      const hmac = crypto.createHmac(hashAlgorithm, secret)
      const digest = Buffer.from(hashAlgorithm + '=' + hmac.update(chunk).digest('hex'), 'utf8')

次に、isAllowedイベント内でreq.on定数を宣言し、x-hub-signature-256ヘッダーが計算した署名と一致するかどうかを示します。crypto.timingSafeEqual関数を使って2つの変数を比較します。この関数は、攻撃者が値を推測する手がかりとなる処理時間の情報を漏らさずに比較できます。

      const isAllowed = (signature.length === digest.length && crypto.timingSafeEqual(digest, signature));
      if (!isAllowed) {
        const msg = 'Webhook signature does not match the hash of the payload';
        console.log(msg);
        res.writeHead(401)
        res.end(msg)
        return;
      }

上記のコードで一致が確認できれば、リクエストは悪意のある攻撃者ではなく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コードが返されます。

悪意のあるJSONペイロードを含むPostmanリクエストと、Webhookの署名がペイロードのハッシュと一致しないことを示す401 Unauthorizedレスポンス

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

「My WebHook Test」と「This is a webhook test.」というテキストが表示された、webhook-testというリポジトリのGitHub README.mdエディター。

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

GitHubの「変更をコミット」ダイアログ。「README.mdを更新」というメッセージと、紫色の矢印で示されたメインブランチに直接コミットするオプションが表示されています。

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

GitHubのWebhookプッシュイベント、リポジトリの更新、Git pullの成功を示すターミナル出力。

まとめ

Webhookを使うとツール間で通信でき、イベント発生時にリアルタイムのデータを受け取れます。システムにWebhookを統合すれば、柔軟で拡張性の高いコラボレーションワークフローを実現できます。定義済みのパラメーターを使うAPIでは、こうしたワークフローを実現できません。

Webhookは設計上公開されるため、セキュリティ上の脅威にさらされやすく、サプライチェーン攻撃を防ぐための適切な対策が欠かせません。

Webhook署名は推奨される解決策です。Webhookプロバイダーがシークレットを設定し、強力なハッシュアルゴリズムを使って署名を生成できるためです。署名は、Webhook間のデータ通信が検証済みの信頼できる相手との間でのみ行われることを確認する、シンプルで安全な方法です。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。

続きを読む

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに対応するセキュリティ

AI駆動型開発のスピードに、従来のセキュリティワークフローは追いついていません。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

blog feature ai violet
Blog

VulnBump:AIエージェントはビルドを壊さずに脆弱な依存関係を修正できるか?

VulnBumpは、実際の依存関係アップグレード307件を対象にAIエージェントをテストします。脆弱性が解消され、ビルドとテストが成功した場合にのみ、修正としてカウントされます。

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。