Stripe webhook hardening
@shipcheck/stripe-webhook-hardening
Find your Stripe webhook handlers and harden them: verify the Stripe-Signature header, use the raw body, keep the signing secret in env, process each event once, reject bad signatures with 400, and fulfill orders from the webhook rather than the success page. Reads your code and proposes diffs; never calls Stripe.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1{
- 2 "name": "stripe-webhook-hardening",
- 3 "owner": "shipcheck",
- 4 "version": "1.0.0",
- 5 "type": "skill",
- 6 "description": "Find your Stripe webhook handlers and harden them: verify the Stripe-Signature header, use the raw body, keep the signing secret in env, process each event once, reject bad signatures with 400, and fulfill orders from the webhook rather than the success page. Reads your code and proposes diffs; never calls Stripe.",
- 7 "category": "devops",
- 8 "license": "MIT",
- 9 "compat": ["claude-code"],
- 10 "permissions": {
- 11 "shell": ["node scripts/webhook-scan.mjs"],
- 12 "files": {
- 13 "read": ["./**"],
- 14 "write": ["./out/**", "./app/**", "./src/**", "./pages/**", "./server/**", "./api/**", "./supabase/functions/**", "./supabase/migrations/**"]
- 15 }
- 16 },
- 17 "tests": {
- 18 "cases": [
- 19 {
- 20 "name": "flags an App Router handler that parses JSON and never verifies the signature",
- 21 "inputs": {
- 22 "project/app/api/webhooks/stripe/route.ts": "export async function POST(req: Request) {\n const event = await req.json();\n if (event.type === \"checkout.session.completed\") {\n await markPaid(event.data.object.id);\n }\n return Response.json({ received: true });\n}\n"
- 23 },
- 24 "checks": [
- 25 {
- 26 "type": "command_exits_zero",
- 27 "command": "node scripts/webhook-scan.mjs project --json out/webhook-scan.json --md out/webhook-scan.md"
- 28 },
- 29 {
- 30 "type": "output_matches_schema",
- 31 "path": "out/webhook-scan.json",
- 32 "schema": {
- 33 "type": "object",
- 34 "required": ["summary", "handlers"],
- 35 "properties": {
- 36 "summary": {
- 37 "type": "object",
- 38 "properties": {
- 39 "handlers": { "const": 1 },
- 40 "high": { "const": 2 },
- 41 "medium": { "const": 1 }
- 42 }
- 43 }
- 44 }
- 45 }
- 46 },
- 47 { "type": "file_contains", "path": "out/webhook-scan.md", "text": "| signature | **FAIL** (high)" },
- 48 { "type": "file_contains", "path": "out/webhook-scan.md", "text": "| raw-body | **FAIL** (high)" }
- 49 ]
- 50 },
- 51 {
- 52 "name": "passes a hardened handler that verifies, uses the raw body, and dedupes on event.id",
- 53 "inputs": {
- 54 "project/app/api/webhooks/stripe/route.ts": "import Stripe from \"stripe\";\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\nexport async function POST(req: Request) {\n const body = await req.text();\n const sig = req.headers.get(\"stripe-signature\")!;\n let event: Stripe.Event;\n try {\n event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);\n } catch {\n return new Response(\"bad signature\", { status: 400 });\n }\n const { error } = await supabase.from(\"processed_events\").insert({ id: event.id });\n if (error) return Response.json({ received: true });\n switch (event.type) {\n case \"checkout.session.completed\":\n await fulfill(event.data.object);\n break;\n }\n return Response.json({ received: true });\n}\n"
- 55 },
- 56 "checks": [
- 57 {
- 58 "type": "command_exits_zero",
- 59 "command": "node scripts/webhook-scan.mjs project --json out/webhook-scan.json"
- 60 },
- 61 {
- 62 "type": "output_matches_schema",
- 63 "path": "out/webhook-scan.json",
- 64 "schema": {
- 65 "type": "object",
- 66 "properties": {
- 67 "summary": {
- 68 "type": "object",
- 69 "properties": {
- 70 "handlers": { "const": 1 },
- 71 "high": { "const": 0 },
- 72 "medium": { "const": 0 },
- 73 "low": { "const": 0 }
- 74 }
- 75 }
- 76 }
- 77 }
- 78 }
- 79 ]
- 80 },
- 81 {
- 82 "name": "flags sync constructEvent on the edge runtime and a hardcoded signing secret",
- 83 "inputs": {
- 84 "project/app/api/stripe/route.ts": "export const runtime = \"edge\";\nexport async function POST(req: Request) {\n const body = await req.text();\n const event = stripe.webhooks.constructEvent(body, req.headers.get(\"stripe-signature\"), \"whsec_FAKEFORTESTS\");\n return new Response(\"ok\");\n}\n"
- 85 },
- 86 "checks": [
- 87 {
- 88 "type": "command_exits_zero",
- 89 "command": "node scripts/webhook-scan.mjs project --md out/webhook-scan.md"
- 90 },
- 91 { "type": "file_contains", "path": "out/webhook-scan.md", "text": "| edge-runtime | **FAIL** (medium)" },
- 92 { "type": "file_contains", "path": "out/webhook-scan.md", "text": "| secret-from-env | **FAIL** (high)" }
- 93 ]
- 94 }
- 95 ]
- 96 }
- 97}
