Skip to content
BuildThenTestedSee packages
CriticalPayments

Stripe webhook accepted without signature verification

Anyone who finds the webhook URL can post a made-up payment-succeeded event and get an order fulfilled or a plan activated for a user of their choosing, without paying. Everything between the pay button and the money arriving. Check it yourself with the steps below; our test adds what a script and a second account can find.

What it looks like

// app/api/stripe/webhook/route.ts
export async function POST(req: Request) {
  const event = await req.json();          // body trusted as-is
  if (event.type === "checkout.session.completed") {
    await markPaid(event.data.object.client_reference_id);
  }
  return new Response("ok");
}
A webhook handler (the route Stripe calls to report events) that reads the request body as a Stripe event without checking the Stripe-Signature header.

What happens

Anyone who finds the webhook URL can post a made-up payment-succeeded event and get an order fulfilled or a plan activated for a user of their choosing, without paying.

Where it shows up

Descriptive, not a verdict: builders and stacks whose defaults make this reachable, or that a public incident names. Each hub states the tool's own security features.

Check it yourself

Uses your builder's dashboard and your browser. No tools to install.

  1. In the Stripe Dashboard, open Developers, then Webhooks, and copy the endpoint URL your app uses. It usually ends in something like /api/stripe/webhook or /functions/v1/stripe-webhook.
  2. In your builder, open the file that handles that route, or ask the builder's chat to show it. Look for a call named constructEvent or construct_event that receives a value starting whsec_.
  3. In the builder's environment or secrets settings, confirm a variable such as STRIPE_WEBHOOK_SECRET exists and is not blank.
  4. Bad result: the handler parses the body with req.json() or JSON.parse and never calls a construct-event function, or the webhook secret variable is missing or empty.

What we check

We send an unsigned event and an event signed with the wrong secret to the live endpoint and confirm both are rejected. We then post a forged checkout.session.completed for a test account and check whether any plan, order or credit balance changes.

Fix direction

Read the raw request body, pass it with the Stripe-Signature header and the endpoint's whsec_ secret to the Stripe library's construct-event function, and reject the request when verification fails. Stripe's docs also describe restricting the route to Stripe's published IP addresses as a second layer.

Mechanism documented in: Receive Stripe events in your webhook endpoint (verified 2026-09-21); Resolve webhook signature verification errors (verified 2026-09-21).

Covered by

Ready-to-charge check ($390, 48 hours) and Full audit ($1,900, 5 business days)

Both include a re-check after your fixes.

Compare packages

Related patterns

Questions

stripe webhook signature verification missing

Stripe signs every event it sends with a per-endpoint secret and puts the signature in the Stripe-Signature header. If your handler skips the check, it cannot tell a Stripe event from one typed by a stranger. Search the handler for a construct-event call and a whsec_ secret; if neither is there, the check is missing.

stripe webhook forged events

A forged event is a request to your webhook URL that looks like Stripe's JSON but was written by someone else. Stripe's own docs say such requests can trigger order fulfilment, account access or record changes if the endpoint does not verify the signature. Verification with the endpoint secret is the control that stops it.

stripe webhook secret empty bypass

Some generated handlers only verify when the secret variable is set, so an empty STRIPE_WEBHOOK_SECRET silently turns verification off. Check the production environment for a non-empty value that matches the live endpoint in the Stripe Dashboard, and make the handler fail closed when the secret is absent.

Back to the catalogue, or run the self-check for your stack.