Stripe Webhook セキュリティ
このスキルは、Stripe に関連する vibe-preflight スキャナー(このファイルの隣にある scripts/preflight.mjs、Node 20+、依存関係なし、ネットワーク不要)を実行します。Next.js ルートハンドラ、pages/api、Express、Supabase Edge Functions 内の Webhook ハンドラを検出し、コード内の 2 つの項目をチェックします。このページの残りの部分は、ハンドラを読んで行うレビューです。パターンスキャンだけでは証明できないからです。
1. スキャンを実行する
プロジェクトルートから実行します:
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only stripe --fail-on warning --format json
Claude Code は ${CLAUDE_SKILL_DIR} を自動で補完します。他のエージェント: このスキルのフォルダ(例: .agents/skills/stripe-webhook-security または .claude/skills/stripe-webhook-security)を使用してください。
| ルール | レベル | 意味 |
|---|---|---|
stripe-webhook-unverified | error | ハンドラが Stripe イベントを処理しているが、constructEvent / constructEventAsync をまったく呼び出していません。URL を知っている誰でも偽の checkout.session.completed を POST できます。 |
stripe-webhook-parsed-body | warning | 署名はチェックされていますが、すでに解析済みのボディに対して行われています。Stripe は送信した正確なバイト列に署名するため、実際のイベントは 400 で失敗し、誰かが結局チェックを削除することになります。 |
--only stripe は、Stripe のイベント名、stripe-signature ヘッダーの読み取り、または Stripe に言及しているファイル内の Webhook パスによってハンドラを見つけます。0 errors と報告された場合でも、ユーザーが Webhook を持っていることがわかっていれば、自分でハンドラを見つけて、以下のレビューを行ってください。
2. 署名検証を修正する
ルートハンドラ(Next.js App Router、Remix、Hono などの fetch スタイルハンドラ):
export async function POST(request: Request) {
const body = await request.text(); // Raw ボディ、絶対に request.json() は使わない
const signature = request.headers.get("stripe-signature");
if (!signature) return new Response("署名がありません", { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("署名が無効です", { status: 400 });
}
// イベントを処理
return Response.json({ received: true });
}
Express: app.use(express.json()) より前に express.raw({ type: "application/json" }) で Webhook ルートを登録し、req.body(この場合は Buffer)を constructEvent に渡します。
Next.js pages/api: export const config = { api: { bodyParser: false } }; と設定し、検証前に Raw リクエストを Buffer として読み取ります。
Supabase Edge Functions およびその他の Deno / エッジランタイム:
await stripe.webhooks.constructEventAsync(body, signature, secret, undefined, Stripe.createSubtleCryptoProvider())。
3. ハンドラを読んで残りをレビューする
スキャナーはこれらをチェックしません。コードを見て、各項目について見つけたことをユーザーに伝えてください。
- シークレット。
STRIPE_WEBHOOK_SECRET(whsec_...)はサーバー環境変数から取得し、NEXT_PUBLIC_/VITE_変数やソースコードからは絶対に取得しないでください。テストモードとライブモードでは異なるシークレットを使用し、エンドポイントごと、stripe listenCLI ごとにも異なるシークレットがあります。 - リプレイ。
constructEventは、許容時間(デフォルト 300 秒)を超えた署名を拒否します。許容時間の引数は指定しないか、小さく保ってください。エラーをキャッチしてそのまま続行しないでください。 - 冪等性。 Stripe は同じイベントを複数回配信することがあります。書き込みを自然にリトライ可能にしてください(Stripe ID をキーにした upsert、クレジットを追加する代わりにステータスを設定するなど)。または
event.idを一意制約付きのテーブルに保存し、見たことのある ID はスキップしてください。クレジット、残高、在庫を追加する処理は特に注意深く確認してください。 - 順序。 イベントは順序通りに到着しないことがあります。サブスクリプションの場合、イベントの順序を信頼する代わりに、API から現在のオブジェクトを取得するか(
stripe.subscriptions.retrieve)、タイムスタンプを比較してください。 - 信頼。 ユーザーとプランは検証済みイベント(
client_reference_id、metadata、チェックアウト時に保存した顧客 ID)から取得し、クエリ文字列や署名されていない 2 回目のリクエストからは絶対に取得しないでください。成功ページのリダイレクトは何も証明しません。アクセス権は Webhook から付与してください。 - レスポンス。 イベントが保存または処理されたら、すぐに 2xx を返してください。時間のかかる処理はキューに送ります。400 を返すのは不正な署名の場合のみにし、Stripe のリトライが意味を持つようにしてください。
- イベントリスト。 エンドポイントはコードが処理するイベントのみにサブスクライブし、他のタイプはエラーにせず無視してください。
4. 報告する
スキャナーが見つけた結果を列挙し、その後レビュー項目ごとに 1 行ずつ: 問題なし、修正済み、またはユーザーへの質問(例: 「配信のたびにクレジットが追加されています: 繰り返しイベントで再度追加されますか?」)を記載します。修正後、スキャンを再実行し、その最終行を表示してください。実際にテストするには、Stripe CLI で stripe listen --forward-to localhost:3000/api/webhooks と stripe trigger checkout.session.completed を提案してください。
確認された誤検出は、該当行またはその上の行に // vibe-preflight-ignore <ルールID> をコメントとともに追加することで抑制できます。