Stripe Webhook 安全性
此技能會執行 vibe-preflight 掃描器中的 Stripe 部分(腳本位於 scripts/preflight.mjs 中,與本檔案同目錄,Node 20+,無依賴,無網路需求)。它會在 Next.js 路由處理器、pages/api、Express 和 Supabase Edge Functions 中尋找 Webhook 處理器,並檢查程式碼中的兩項安全規則。本頁面的其餘部分是您需要透過閱讀處理器來進行的審查,因為模式掃描無法證明一切。
1. 執行掃描
從專案根目錄執行:
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only stripe --fail-on warning --format json
Claude Code 會自動填入 ${CLAUDE_SKILL_DIR}。其他 AI Agent:請使用此技能的資料夾路徑,例如 .agents/skills/stripe-webhook-security 或 .claude/skills/stripe-webhook-security。
| 規則 | 等級 | 說明 |
|---|---|---|
stripe-webhook-unverified | error | 處理器處理了 Stripe 事件,但從未呼叫 constructEvent / constructEventAsync。任何知道此 URL 的人都可以 POST 一個假的 checkout.session.completed 事件。 |
stripe-webhook-parsed-body | warning | 有檢查簽章,但檢查的是已經被解析過的主體。Stripe 簽署的是它發送的原始位元組,因此真實事件會失敗並回傳 400,最後開發者可能會刪除這個檢查。 |
--only stripe 會透過 Stripe 事件名稱、讀取 stripe-signature 標頭或檔案中包含 Stripe 的 Webhook 路徑來尋找處理器。如果掃描報告 0 errors,但使用者知道他們有 Webhook,請自行找到該處理器,並仍然執行下方的審查。
2. 修復驗證
Route 處理器 (Next.js App Router、Remix、Hono 及其他 fetch 風格的處理器):
export async function POST(request: Request) {
const body = await request.text(); // 原始主體,絕不使用 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:使用 express.raw({ type: "application/json" }) 註冊 Webhook 路由,務必在 app.use(express.json()) 之前,然後將 req.body(此時為 Buffer)傳遞給 constructEvent。
Next.js pages/api:設定 export const config = { api: { bodyParser: false } };,並在驗證前將原始請求讀取為 Buffer。
Supabase Edge Functions 及其他 Deno / Edge 執行環境:
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),絕不能來自查詢字串或未簽章的第二個請求。成功頁面的重新導向無法證明任何事:請透過 Webhook 授予存取權限。 - 回應。 一旦事件被儲存或處理完畢,應立即回傳 2xx。耗時的操作應放入佇列處理。僅在簽章錯誤時回傳 400,這樣 Stripe 的重試機制才有意義。
- 事件列表。 端點應只訂閱程式碼實際處理的事件類型,並忽略其他類型的事件而不產生錯誤。
4. 報告
列出掃描器的發現結果,然後針對每個審查項目輸出一行:正常、已修復,或向使用者提問(例如「每次送達都增加點數:重複事件會再次增加點數嗎?」)。修復後再次執行掃描,並顯示最後一行輸出。若要進行真實測試,建議使用者使用 Stripe CLI 執行 stripe listen --forward-to localhost:3000/api/webhooks 和 stripe trigger checkout.session.completed。
可以被確認的誤報,可以透過在該行或上一行加上 // vibe-preflight-ignore <rule-id> 來忽略,並在註解中說明原因。