Skip to main content

Webhook 配信を検証する

Webhook シークレットを使用して、Webhook の配信が GitHub からの配信であることを確認できます。

Webhook 配信の検証について

ペイロードを受信するようにサーバーが設定されると、設定したエンドポイントに送信された配信すべてがリッスンされます。 サーバーが GitHub によって送信された Webhook 配信のみを処理し、配信が改ざんされていないことを確認するには、配信をさらに処理する前に webhook 署名を検証する必要があります。 これにより、GitHub からの配信の処理にサーバー時間を費やすのを回避し、中間者攻撃を回避するのに役立ちます。

そのためには、次の手順を実行する必要があります。

  1. Webhook のシークレット トークンを作成します。
  2. トークンをサーバーに安全に格納します。
  3. それらが GitHub から送信されていることをするために、受信 Webhook ペイロードをトークンに対して検証してください。

シークレット トークンの作成

シークレット トークンを使用して新しい Webhook を作成することも、既存の Webhook にシークレット トークンを追加することもできます。 シークレット トークンを作成する際は、エントロピーの高いランダムな文字列を選択してください。

  • _シークレット トークンを使用して新しい Webhook を作成する_には、「webhookの作成」を参照してください。
  • _既存の Webhook にシークレット トークンを追加する_には、Webhook の設定を編集します。 [シークレット] に、secret キーとして使用する文字列を入力します。 詳しくは、「webhookの編集」を参照してください。

シークレット トークンを安全に格納する

シークレット トークンを作成したら、サーバーがアクセスできる安全な場所に格納する必要があります。 トークンをアプリケーションにハードコーディングしたり、トークンをリポジトリにプッシュしたりしないでください。 コードで認証資格情報を安全に使用する方法の詳細については、「API 資格情報をセキュリティで保護する」を参照してください。

Webhook 配信を検証する

GitHub がシークレット トークンを使用して作成したハッシュ署名は、各ペイロードとともに送信されます。 ハッシュ署名は、各配信の X-Hub-Signature-256 ヘッダーの値として表示されます。 詳しくは、「Webhook のイベントとペイロード」を参照してください。

Webhook 配信を処理するコードでは、シークレット トークンを使用してハッシュを計算する必要があります。 次に、GitHub が送信したハッシュを、計算したハッシュの想定値と比較し、それらが一致していることを確認します。 さまざまなプログラミング言語でハッシュを検証する方法を示す例については、「」を参照してください。

Webhook ペイロードを検証する際には、いくつかの重要な点に留意する必要があります。

  • GitHub は、HMAC 16 進ダイジェストを使ってハッシュを計算します。
  • ハッシュ署名は常に、sha256= から始まります。
  • ハッシュ署名は、Webhook のシークレット トークンとペイロードの内容を使用して生成されます。
  • 言語とサーバーの実装で文字エンコーディングが指定されている場合は、ペイロードをUTF-8として扱うようにしてください。 Webhook ペイロードには Unicode 文字を含めることができます。
  • プレーン == 演算子は使用しないでください。 代わりに、「一定時間」の文字列比較を行う secure_comparecrypto.timingSafeEqual などのメソッドを使用して、通常の等価演算子に対する特定のタイミングでの攻撃や、JIT 最適化言語における通常のループを緩和することを検討してください。

Webhook ペイロード検証のテスト

次の secretpayload 値を使用して、実装が正しいことを確認できます。

  • secret: 「誰にも秘密」
  • payload: 「Hello World!」

実装が正しければ、生成するシグネチャは次のシグネチャ値と一致しています。

  • signature: 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
  • X-Hub-Signature-256: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17

選択したプログラミング言語を使用して、コードに HMAC 検証を実装できます。 次に、実装がさまざまなプログラミング言語でどのように表示されるかを示す例をいくつか示します。

Ruby の例

たとえば、次のような verify_signature 関数を定義できます。

def verify_signature(payload_body)
  signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
  return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE_256'])
end

その後、Webhook ペイロードを受信したらそれを呼び出すことができます。

post '/payload' do
  request.body.rewind
  payload_body = request.body.read
  verify_signature(payload_body)
  push = JSON.parse(payload_body)
  "I got some JSON: #{push.inspect}"
end

Python の例

たとえば、次のような verify_signature 関数を定義し、Webhook ペイロードを受信したらそれを呼び出すことができます。

import hashlib
import hmac
def verify_signature(payload_body, secret_token, signature_header):
    """Verify that the payload was sent from GitHub by validating SHA256.

    Raise and return 403 if not authorized.

    Args:
        payload_body: original request body to verify (request.body())
        secret_token: GitHub app webhook token (WEBHOOK_SECRET)
        signature_header: header received from GitHub (x-hub-signature-256)
    """
    if not signature_header:
        raise HTTPException(status_code=403, detail="x-hub-signature-256 header is missing!")
    hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
    expected_signature = "sha256=" + hash_object.hexdigest()
    if not hmac.compare_digest(expected_signature, signature_header):
        raise HTTPException(status_code=403, detail="Request signatures didn't match!")

JavaScript の例

たとえば、次のような verifySignature 関数を定義し、Webhook ペイロード受信時に呼び出すことができます。

let encoder = new TextEncoder();

async function verifySignature(secret, header, payload) {
    let parts = header.split("=");
    let sigHex = parts[1];

    let algorithm = { name: "HMAC", hash: { name: 'SHA-256' } };

    let keyBytes = encoder.encode(secret);
    let extractable = false;
    let key = await crypto.subtle.importKey(
        "raw",
        keyBytes,
        algorithm,
        extractable,
        [ "sign", "verify" ],
    );

    let sigBytes = hexToBytes(sigHex);
    let dataBytes = encoder.encode(payload);
    let equal = await crypto.subtle.verify(
        algorithm.name,
        key,
        sigBytes,
        dataBytes,
    );

    return equal;
}

function hexToBytes(hex) {
    let len = hex.length / 2;
    let bytes = new Uint8Array(len);

    let index = 0;
    for (let i = 0; i < hex.length; i += 2) {
        let c = hex.slice(i, i + 2);
        let b = parseInt(c, 16);
        bytes[index] = b;
        index += 1;
    }

    return bytes;
}

TypeScript の例

たとえば、次のような verify_signature 関数を定義し、Webhook ペイロードを受信したらそれを呼び出すことができます。

JavaScript
import * as crypto from "crypto";

const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET;

const verify_signature = (req: Request) => {
  const signature = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest("hex");
  let trusted = Buffer.from(`sha256=${signature}`, 'ascii');
  let untrusted =  Buffer.from(req.headers.get("x-hub-signature-256"), 'ascii');
  return crypto.timingSafeEqual(trusted, untrusted);
};

const handleWebhook = (req: Request, res: Response) => {
  if (!verify_signature(req)) {
    res.status(401).send("Unauthorized");
    return;
  }
  // The rest of your logic here
};

トラブルシューティング

ペイロードが GitHub から確実に取得されているが、署名の検証が失敗する場合:

  • Webhook のシークレットが構成されていることを確認します。 Webhook のシークレットを構成していない場合、X-Hub-Signature-256 ヘッダーは存在しません。 Webhook シークレットの設定について詳しくは、「webhookの編集」をご覧ください。
  • 正しいヘッダーを使用していることを確認します。 GitHub では、HMAC-SHA256 アルゴリズムを使用する X-Hub-Signature-256 ヘッダーを使用することをお勧めします。 X-Hub-Signature ヘッダーはHMAC-SHA1 アルゴリズムを使用し、従来の目的でのみ含まれています。
  • 正しいアルゴリズムを使用していることを確認します。 X-Hub-Signature-256 ヘッダーを使用している場合は、HMAC-SHA256 アルゴリズムを使用する必要があります。
  • 正しい webhook シークレットを使用していることを確認します。 Webhook シークレットの値がわからない場合は、Webhook のシークレットを更新できます。 詳しくは、「webhookの編集」を参照してください。
  • 検証の前にペイロードとヘッダーが変更されていないことを確認します。 たとえば、プロキシまたは負荷バランサーを使用する場合は、プロキシまたは負荷バランサーがペイロードまたはヘッダーを変更していないことを確認します。
  • 言語とサーバーの実装で文字エンコーディングが指定されている場合は、ペイロードをUTF-8として扱うようにしてください。 Webhook ペイロードには Unicode 文字を含めることができます。

参考資料