❌

Normal view

We probed 6,000 web apps for Stripe webhook signature checks. 1,542 don't bother

Quick note from a scanning project I've been running. We hit 6,000 web apps with a payment-bypass probe last week, sending a minimal fake `checkout.session.completed` event to common webhook paths (`/api/webhook/stripe`, `/api/payments/webhook`, etc.) without a `Stripe-Signature` header.

1,542 returned 200.

That means anyone with curl can fire a forged Stripe event at those endpoints and the server processes it as legitimate. Depending on what the handler does with it, the consequences range from "logs a fake event" to "marks attacker's account as paid" to "creates a confirmed order with no payment".

The split was roughly:

  • Custom domains (real production SaaS): ~720
  • Render: 198
  • Vercel: 142
  • Replit: 121
  • Railway, Fly, Heroku, others: ~360

Why so many?

The Stripe library makes signature verification a one-liner. Every framework has the canonical example. But the dev journey usually goes: build the route locally with a stub handler that just `console.log`s the event body, get the upgrade-the-user logic working, leave signature verification on the TODO, ship. Six months later nobody remembers it was ever a TODO.

The fix in Express:

\``js app.post('/api/webhook/stripe', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { return res.status(400).send(`Webhook Error: ${err.message}`); } // proceed with event res.json({received: true}); }); ````

The trap: `express.json()` globally parses the body before your handler sees it, leaving Stripe's library to compute the signature against parsed-then-stringified JSON, which never matches. Use `express.raw()` specifically on the webhook route, before any global JSON parser.

FastAPI / Python: read `await request.body()` directly, not `request.json()`. Same idea.

Caveats: a 200 response doesn't prove the app actually grants the attacker something. Some endpoints log every webhook for analytics and return 200 regardless. The 1,542 number is "endpoints accepting unsigned events", not "definitely exploitable". But the misconfiguration is real on its own.

Full writeup with the methodology and platform-by-platform breakdown: https://securityscanner.dev/blog/stripe-webhook-signature-bypass-1500-apps

Curious if anyone here has shipped a Stripe webhook recently and can double-check theirs.

submitted by /u/Most_Ad_394
[link] [comments]
❌