r/node • • 8d ago

Webhook signature bugs I found while supporting 40+ providers, including two that let bad requests through

I maintain an open-source webhook verification library, and most of the bugs I've hit weren't in the HMAC. A few that might save someone time:

  1. The body parser runs first. express.json() parses the body, JSON.stringify(req.body) doesn't give back the bytes the provider signed (whitespace, unicode escapes, number formatting), and the check fails with the right secret. Verify the raw body: express.raw(), await request.text(), Fastify's rawBody, NestJS's rawBody: true. Comparing the body length with content-length is a cheap way to detect it.

  2. Deduping on an unsigned header. Providers retry, so you dedupe on an event ID. If that ID comes from a header the signature doesn't cover (x-github-delivery, say), anyone with a captured request can change the header and replay it past your dedupe store. Key on a signed ID or a hash of the body. My library had exactly this bug.

  3. Tests that only agree with themselves. My Paddle signer and verifier both read h=. Paddle sends h1=. Every test passed and every real Paddle webhook failed. Now CI verifies against the official Stripe, Octokit, Svix and Twilio SDKs in both directions, and new providers need a test vector from the provider's docs.

  4. Weak fallback schemes. HubSpot sends v1/v2 signatures next to v3. If you accept any of them, someone can strip the v3 headers from a captured request and it verifies with no timestamp check. Same idea for GitHub's SHA-1 header.

  5. A short dedupe TTL for providers with no signed timestamp. GitHub and Shopify don't sign a timestamp, so dedupe is your only replay protection. A 5-minute TTL means a captured delivery verifies again after 5 minutes.

The library is verihook (TypeScript, zero deps, Node/Deno/Bun/Workers) if you want to see how it handles these: GitHub: https://github.com/creatorpiyush/verihook npm: https://www.npmjs.com/package/verihook

Curious which provider has caused you the most pain.

3 Upvotes

2 comments sorted by

1

u/zomerbk 2d ago

The unsigned delivery ID example is a useful reminder that deduplication and signature verification solve different problems: a body hash can identify identical retries, but it won’t stop the same signed body from being replayed after the record expires. For providers without signed timestamps, keep dedupe records at least as long as your accepted replay window, and make the event handler idempotent so a late replay can’t repeat side effects.