Stripe webhook accepted without signature verification
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");
}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.
- 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.
- In your builder, open the file that handles that route, or ask the builder's chat to show it. Look for a call named
constructEventorconstruct_eventthat receives a value startingwhsec_. - In the builder's environment or secrets settings, confirm a variable such as STRIPE_WEBHOOK_SECRET exists and is not blank.
- Bad result: the handler parses the body with
req.json()orJSON.parseand 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.
Related patterns
- No idempotency on payment handlersPaymentsHigh
A customer is charged twice after a network hiccup, or a replayed webhook tops up an account again and again for a single payment.
- Price or plan taken from the clientPaymentsHigh
A buyer can edit the request in their browser, pay one cent for the top plan, and your app records it as a full purchase.
- Subscription state trusted from local storagePaymentsHigh
Anyone can type one line in the browser console to become a paying customer, and the server hands over the paid feature without checking.
- Stripe test keys in productionPaymentsHigh
Customers can pay with Stripe's published test card numbers, the app fulfils the order, and no money ever reaches your account.
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.