Back to Technical Mode

Payment Webhook Signature Bypass

Payment providers notify your backend that a charge succeeded via a webhook POST. If the endpoint trusts the request body without verifying the provider's signature, anyone who finds the URL can fabricate a 'payment succeeded' event and get an order fulfilled for free.

A webhook URL is a public endpoint — 'nobody will guess it' is not a security control
Verification must use the RAW request body bytes, computed before any JSON parsing — re-serializing a parsed object frequently changes whitespace/key order and silently breaks the signature check
The signing secret is provider-specific and per-webhook-endpoint — reusing an API key for signature verification is a common, dangerous shortcut
Idempotency keys prevent a legitimately-signed event from being replayed and processed twice, which signature verification alone does not stop

Want this flow explained in detail as it plays?

POST/webhooks/stripe

Headers

Host:app.flowground.dev
Stripe-Signature:t=1699999999,v1=<hmac>

Body

Loading...

Quick-Insert Payloads

Defense in depth

  • Verify the provider's signature on every incoming webhook using the raw, unparsed request body and the endpoint-specific signing secret before trusting anything in it
  • Reject events whose timestamp is outside a small tolerance window (a few minutes), to limit how long a captured signed event remains replayable
  • Track processed event IDs (idempotency) and ignore any event ID already fulfilled, independent of whether its signature is valid

Vulnerable vs. Fixed

app.post("/webhooks/stripe", express.json(), async (req, res) => {
  const event = req.body; // parsed JSON trusted directly, no signature check at all
  if (event.type === "checkout.session.completed") {
    await fulfillOrder(event.data.object.metadata.orderId);
  }
  res.json({ received: true });
});