Why does my Stripe webhook accept unsigned events?
Because nothing in your handler rejects them. Stripe signs every event in the Stripe-Signature header, but checking that signature is up to your code: unless the handler passes the raw request body, that header and the endpoint's whsec_ signing secret to the official library (constructEvent in Node, construct_event in Python and Ruby) and returns 400 when it throws, any POST with plausible JSON gets processed as if Stripe sent it.
Stripe's own docs spell out what that allows: an attacker can send fake events to fulfil orders, grant account access or modify records.
The usual causes
The handler parses the body and switches on event.type without ever calling the verify function.
Verification only runs if a secret is configured (if (endpointSecret) { … }, a pattern that appears in Stripe's own sample code), and the variable is missing in production, so the check is silently skipped.
Verification throws, the error is caught and logged, and the handler carries on and returns 200.
The framework parsed the body before verification, so every real event fails, and the "fix" was to remove the check. Verify against the raw body instead; in Express, register the webhook route before express.json().
A tolerance of 0, which disables the timestamp check that stops replayed events. The libraries default to five minutes.
Three tests
Run these against a staging or preview deploy you own. If the handler really is unverified, the first request will be treated as a real event.
stripe listen prints its own whsec_ secret, which is different from the Dashboard endpoint's secret. Use the matching one in each environment.
While you're in there
Stripe can deliver the same event more than once. Record processed event IDs and skip repeats.
Return 2xx quickly and do slow work afterwards, or deliveries time out and get retried.
Ship Check Kit · $29
Rather not run it by hand?
The Ship Check Kit has your coding agent test this on your app: SC-PAY-01 sends an unsigned and a badly signed fake event to your webhook and flags handlers that never verify, and SC-PAY-03 checks duplicate and out-of-order event handling. Two of 44 launch checks, each with the evidence and the fix.