Last week we scanned roughly 6,000 web apps with our payment-bypass module. The module is dumb on purpose. It POSTs a minimal fake checkout.session.completed event to a list of common webhook paths and asks one question: does the server accept it without a Stripe-Signature header? 1,542 apps said yes. That is not a typo. One in four apps with a payment-webhook-shaped URL is willing to process a forged Stripe event from any HTTP client on the internet. No auth, no signature, no replay protection. What we actually sent The payload is whatever Stripe's documentation says a real event looks like, minus the cryptographic proof: POST /api/webhook/stripe HTTP/1.1 Host: example.com Content-Type: application/json { "id": "evt_secprobe_test", "object": "event", "type": "checkout.session.completed", "data": { "object": { "id": "cs_secprobe_test", "payment_status": "paid", "amount_total": 100, "customer": "cus_secprobe", "currency": "usd" } }, "livemode": false } That is it. No Stripe-Signature header. A real Stripe event arrives with a header that looks like t=1234567890,v1=hexdigest... , computed by Stripe using a secret you set up when you registered the webhook endpoint. If your server skips the signature check, it has no way to know whether the event came from Stripe, from us, or from anyone with curl. We tried 17 common path variants per host: /api/webhooks/stripe , /api/webhook/stripe , /api/payments/webhook , /webhooks/stripe , plus the same patterns for Paddle and LemonSqueezy. If the server returned 200, 201, or 202, we counted it. The exploit primitive Why this matters more than a generic "missing auth" finding: Stripe webhooks are how payment status reaches the application. A typical SaaS flow looks like this: User clicks "subscribe", redirects to Stripe Checkout. User pays. Stripe redirects them back. Stripe also sends a checkout.session.completed webhook to your server. Your server reads the event, looks up the customer email or session ID, and flips that user's account from plan: free to plan: pro . If step 4 doesn't verify the signature, anyone can fire step 4 directly. They sign up for the free tier, find the webhook URL (often documented or trivially guessable), and POST a fake event with their own customer ID. Their account upgrades. Stripe never charged them. The variant that hits hardest: apps that look up the user from a field inside the event body (the customer email or session ID). The attacker doesn't even need a session, just the URL. Stuff their email in the fake event, hit send, get pro. Why is this so common? Stripe's documentation is good and the example code includes the signature check. Every major framework has a one-line library function for it. Yet a quarter of webhook endpoints we scanned don't run it. The pattern we keep seeing in the developer journey: Build the integration locally with a stub handler that just console.log s the event body. Get the upgrade-the-user logic working. Signature verification is on the TODO. Deploy to production. The signature check is still on the TODO. It works. Real Stripe events come through. Customers pay, accounts upgrade. The TODO stays. Six months later, no one remembers it was ever a TODO. The same pattern shows up on apps generated by code assistants. The generated route handler accepts JSON, parses the event, calls the upgrade function. Signature verification is a separate idea the developer has to remember to ask for. Most don't. Spread by hosting platform The 1,542 hits aren't concentrated on any one platform. Roughly half are on custom domains (production SaaS apps), half on hosted preview platforms. A few buckets: Custom domains: ~720 hits Render ( onrender.com ): 198 Vercel: 142 Replit: 121 Railway: 87 Fly.dev: 64 Heroku: 58 Lovable, Bolt, Netlify, others: ~150 combined Custom-domain SaaS apps are the most worrying bucket because they are real businesses with real Stripe accounts. The hosted-preview hits are usually less serious. Many are demo apps, half-built side projects, or test deployments. But they still expose the same code patterns the developer will copy into production. One anonymized example One of the cleanest hits was a hotel booking site on Render. Their webhook endpoint at /api/webhook/paddle returned 200 OK with an empty body to our forged event. The body told us nothing, but the 200 told us everything: the server accepted, parsed, and presumably acted on a Paddle event we made up. We didn't follow up to confirm the exploit (sending a real fake-customer payload would cross from "scanning" to "actively defrauding"), but the primitive is clear. A guest could craft an event that says "this booking is paid", POST it, and the booking record flips to confirmed. The hotel sees a confirmed reservation in their dashboard. The guest never paid. We disclosed to the operator privately. They acknowledged within 4 hours. The six-line fix Stripe's library does this for you. In Node: app.post('/api/webhook/stripe', express.raw({type: 'appl...
A widespread vulnerability exists where web applications fail to validate Stripe webhook signatures, allowing unauthenticated attackers to forge payment completion events and fraudulently upgrade accounts. The attack vector involves sending a minimal POST request mimicking a `checkout.session.completed` event to common webhook paths without the required `Stripe-Signature` header. The article's scan of 6,000 apps found 1,542 (approximately 25%) vulnerable, with exploitation requiring only the webhook URL and a crafted payload containing the attacker's customer identifier.