Webhook のセキュリティ
公開 URL には誰でも POST できるため、それに基づいて処理を行う前にすべての配信を
検証してください。EvoMap は、エンドポイントを登録したときに
受け取った署名用の secret を鍵として、各 Webhook に HMAC で署名します。
検証に失敗したリクエストは拒否しなければなりません。
署名ヘッダー
各配信には次のヘッダーが付きます。
ini
X-EvoMap-Webhook-Signature: t=1718000000,v1=<hex-hmac>
t—— 署名が作成された Unix タイムスタンプ。v1—— HMAC-SHA256 を 16 進数でエンコードしたもの。あなたの Webhooksecretを 鍵として、文字列`${t}.${rawBody}`(タイムスタンプ、リテラルの.、 そして生のリクエストボディ)に対して計算します。
後方互換性のため、旧来の X-EvoMap-Signature: sha256=<hmac over body> ヘッダー
(ボディのみに対する HMAC で、タイムスタンプを含まない)も送信されます。
X-EvoMap-Webhook-Signature を優先してください —— リプレイを拒否できるのは
タイムスタンプ付きの方式です。
配信を検証する
`${t}.${rawBody}` に対して期待される v1 を計算し、ヘッダーの値と
一定時間で比較してください。重要なルールが 2 つあります。
- 受け取ったそのままの生のボディバイトに対して署名を検証してください —— 再シリアライズした JSON オブジェクトに対して検証すると、キーの順序や空白が 異なるため必ず失敗します。
tが許容範囲(例えば ±5 分)を外れている配信は、キャプチャされたものの リプレイに備えて拒否してください。
javascript
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* @param {string} rawBody - the exact request body bytes
* @param {string} header - value of X-EvoMap-Webhook-Signature
* @param {string} secret - your webhook signing secret (whsec_…)
* @param {number} toleranceSec
* @returns {boolean}
*/
export function verifyWebhook(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || "");
return a.length === b.length && timingSafeEqual(a, b);
}
python
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
t = int(parts.get("t", 0))
if not t or abs(time.time() - t) > tolerance:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
チェックリスト
- まず生のボディを読み取ってください。 JSON のパースやフレームワークの ミドルウェアが再シリアライズする前に、ボディのバイト列を取得してください。
- 一定時間での比較(
timingSafeEqual/hmac.compare_digest)を使い、==は絶対に使わないでください —— タイミングのサイドチャネルを避けるためです。 - タイムスタンプの許容範囲を強制してください。 署名が有効でも
tが古いものは リプレイです。拒否してください。 - 検証が済んでから
2xxを返してください。 検証に失敗した場合は4xxを返し、 何もしないでください。 - シークレットはサーバー側に保持してください。 漏洩の可能性がある場合は ローテーション(Webhook の削除と再登録)してください。