CriticalSecurityAuto-fixable

Unverified Stripe Webhook

The Stripe webhook endpoint accepts any POST without verifying the signature header, allowing an attacker to forge subscription events, credit accounts, or cancel subscriptions.

Typical error

Webhook signature not checked

What this is

Stripe signs every webhook request with a secret you configured in the Stripe dashboard. Your endpoint is supposed to verify that signature before trusting the event. Without verification, anyone who knows the endpoint URL can POST fake events.

Consequences:

  • Forge checkout.session.completed and grant a Pro plan for free
  • Forge customer.subscription.deleted and cancel another user's plan
  • Forge invoice.payment_succeeded and trip business logic that depends on real payment

Why AI tools ship this

The generated webhook handler looks at req.body and routes on event.type. It skips the stripe.webhooks.constructEvent() step because that requires reading the raw body, which has framework-specific gotchas.

How to detect

Search for Stripe webhook routes:

grep -rE "stripe.*webhook" --include="*.ts" --include="*.tsx" .

Read each one. If it does not call stripe.webhooks.constructEvent() with the raw body and the signature header, it is unverified.

How to fix

Next.js App Router pattern:

import Stripe from 'stripe'
import { headers } from 'next/headers'
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
 
export async function POST(req: Request) {
  const rawBody = await req.text()
  const sig = (await headers()).get('stripe-signature')
 
  if (!sig) {
    return new Response('Missing signature', { status: 400 })
  }
 
  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(rawBody, sig, webhookSecret)
  } catch {
    return new Response('Invalid signature', { status: 400 })
  }
 
  // now event is safe to trust
  switch (event.type) {
    case 'checkout.session.completed':
      // handle
      break
  }
 
  return new Response('ok', { status: 200 })
}

Critical: use req.text() to read the raw body. Parsing JSON first breaks signature verification.

Commonly affected tools

Glossary

Is your app affected?

FinishKit checks for this finding and 50+ more across 8 dimensions of production readiness. Free during beta.

Scan your app