Webhook 配信の検証について
ペイロードを受信するようにサーバーが設定されると、設定したエンドポイントに送信された配信すべてがリッスンされます。 サーバーが GitHub によって送信された Webhook 配信のみを処理し、配信が改ざんされていないことを確認するには、配信をさらに処理する前に webhook 署名を検証する必要があります。 これにより、GitHub からの配信の処理にサーバー時間を費やすのを回避し、中間者攻撃を回避するのに役立ちます。
そのためには、次の手順を実行する必要があります。
- Webhook のシークレット トークンを作成します。
- トークンをサーバーに安全に格納します。
- それらが GitHub から送信されていることをするために、受信 Webhook ペイロードをトークンに対して検証してください。
シークレット トークンの作成
シークレット トークンを使用して新しい Webhook を作成することも、既存の Webhook にシークレット トークンを追加することもできます。 シークレット トークンを作成する際は、エントロピーの高いランダムな文字列を選択してください。
- _シークレット トークンを使用して新しい Webhook を作成する_には、「webhookの作成」を参照してください。
- _既存の Webhook にシークレット トークンを追加する_には、Webhook の設定を編集します。 [シークレット] に、
secret
キーとして使用する文字列を入力します。 詳しくは、「webhookの編集」を参照してください。
シークレット トークンを安全に格納する
シークレット トークンを作成したら、サーバーがアクセスできる安全な場所に格納する必要があります。 トークンをアプリケーションにハードコーディングしたり、トークンをリポジトリにプッシュしたりしないでください。 コードで認証資格情報を安全に使用する方法の詳細については、「API 資格情報をセキュリティで保護する」を参照してください。
Webhook 配信を検証する
GitHub Enterprise Server がシークレット トークンを使用して作成したハッシュ署名は、各ペイロードとともに送信されます。 ハッシュ署名は、各配信の X-Hub-Signature-256
ヘッダーの値として表示されます。 詳しくは、「Webhook のイベントとペイロード」を参照してください。
Webhook 配信を処理するコードでは、シークレット トークンを使用してハッシュを計算する必要があります。 次に、GitHub が送信したハッシュを、計算したハッシュの想定値と比較し、それらが一致していることを確認します。 さまざまなプログラミング言語でハッシュを検証する方法を示す例については、「例」を参照してください。
Webhook ペイロードを検証する際には、いくつかの重要な点に留意する必要があります。
- GitHub Enterprise Server は、HMAC 16 進ダイジェストを使ってハッシュを計算します。
- ハッシュ署名は常に、
sha256=
から始まります。 - ハッシュ署名は、Webhook のシークレット トークンとペイロードの内容を使用して生成されます。
- 言語とサーバーの実装で文字エンコーディングが指定されている場合は、ペイロードをUTF-8として扱うようにしてください。 Webhook ペイロードには Unicode 文字を含めることができます。
- プレーン
==
演算子は使用しないでください。 代わりに、「一定時間」の文字列比較を行うsecure_compare
やcrypto.timingSafeEqual
などのメソッドを使用して、通常の等価演算子に対する特定のタイミングでの攻撃や、JIT 最適化言語における通常のループを緩和することを検討してください。
Webhook ペイロード検証のテスト
次の secret
と payload
値を使用して、実装が正しいことを確認できます。
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 ペイロードを受信したらそれを呼び出すことができます。
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 };
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 文字を含めることができます。