- Symptom
- The webhook handler parses the JSON body directly and never reads the Stripe-Signature headerOrders are fulfilled or plans upgraded for payments that do not exist in your Stripe account
- Cause
- Your webhook endpoint is a public URL, so anyone can POST a body shaped like a Stripe event. Without verification, an attacker could send fake events to trigger actions like fulfilling orders, granting account access or modifying records. Verification needs the exact raw body; a framework that parses or changes the body first makes every check fail.
- Fix
- Verify every event with stripe.webhooks.constructEvent, passing the raw body, the Stripe-Signature header and the endpoint's whsec_ signing secret, and return 400 when it throws. In a Next.js App Router handler read the body with request.text(), not request.json(). The secret printed by stripe listen differs from the Dashboard endpoint's secret, so use the one that matches where events come from. Stripe also recommends accepting webhook requests only from its published IP addresses, alongside signature verification.
- Apply
import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!) export async function POST(request: Request) { const body = await request.text() const signature = request.headers.get('stripe-signature') ?? '' let event: Stripe.Event try { event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!) } catch (err) { return new Response(`Webhook Error: ${(err as Error).message}`, { status: 400 }) } // handle event.type here return new Response(null, { status: 200 }) }- Affected
- Stripe webhook endpoints that act on events without signature verification
- Sources
Last verified 2026-10-27 · Confirmed by 0 builders