{ "summary": { "handlers": 1, "high": 2, "medium": 1, "low": 0 }, "handlers": [ { "file": "app/api/webhooks/stripe/route.ts", "checks": [ { "id": "signature", "status": "fail", "severity": "high", "line": 3, "message": "Handles Stripe events without verifying the signature. Anyone who finds this URL can post a fake checkout.session.completed and get your product free.", "fix": "Call stripe.webhooks.constructEvent(rawBody, signature, secret) before reading the event, with the secret from your STRIPE_WEBHOOK_SECRET env var." }, { "id": "raw-body", "status": "fail", "severity": "high", "line": 2, "message": "Reads the body as parsed JSON. Signature checks need the exact raw bytes, so verification fails (or was removed to make it pass).", "fix": "Next.js App Router: const body = await req.text(). Express: express.raw({ type: 'application/json' }) on this route only. Pages Router: export const config = { api: { bodyParser: false } }." }, { "id": "idempotency", "status": "fail", "severity": "medium", "line": 0, "message": "No sign of de-duplicating by event.id. Stripe retries and can deliver the same event twice, so orders, credits, or emails can double up.", "fix": "Insert event.id into a processed_events table with a unique constraint first; if the insert conflicts, return 200 and stop." }, { "id": "event-types", "status": "pass", "severity": null, "line": 3, "message": "Branches on event.type.", "fix": null } ] } ] }