The hook
Webhook signature verification is one of the most consistently-skipped steps in integration code, and the reason is structural: the integration works fine without it. The webhook arrives, the body parses, the handler runs, the test passes. Verification is the step you only notice when an attacker posts a forged event to your endpoint. Stripe, GitHub, Clerk, Resend, Linear, Slack, Vercel, and every other service worth integrating with sets a signature header — `Stripe-Signature`, `X-Hub-Signature-256`, `svix-id`, etc. — and explicitly tells you to verify before trusting the payload. The instructions are in the documentation. The skipping is in the codebase.
यसले कसरी काम गर्छ
Each provider computes an HMAC over the raw request body (using a secret you configure on their side), serializes the result into a header, and sends both. Your handler is supposed to: read the raw body (not the parsed body — parsing changes whitespace and breaks the HMAC), recompute the HMAC using your shared secret, compare with the header using a timing-safe equality function, then trust the body. Skip the check and you accept any payload from any caller — Stripe's webhook URL is not authenticated by IP, secret token in URL, or any other mechanism; the signature is the only authentication. The mistakes cluster in three shapes: never adding the verification call (the handler trusts everything), parsing the body before verifying (the HMAC fails because the parsed-and-reserialized body has different whitespace), and using non-timing-safe comparison (`===` against the signature is technically right but timing-leaks the secret bit-by-bit).
The variants
No signature check at all
Handler parses and trusts every incoming POST. Most common shape; trivially exploitable by anyone who knows the URL.
Verify-after-parse
Handler parses body to JSON, then computes HMAC over the JSON.stringify version. Whitespace differs from the original; HMAC fails. Verification 'works' for legit events because both sides round-trip the same way, but is broken for production-grade attackers.
Non-timing-safe comparison
`if (signature === expectedSignature)` instead of `crypto.timingSafeEqual(...)`. Leaks one bit per request through timing analysis. Less common in practice but a genuine vulnerability.
Replay without timestamp check
Even with HMAC verification, accepting old timestamps lets an attacker replay captured events. Stripe's signature includes a timestamp; rejecting events older than 5 minutes defeats replay.
The blast radius
Forged events. Stripe webhook fakery: an attacker posts a fake `payment_intent.succeeded` event to your `/api/stripe/webhook` endpoint, your handler marks the order as paid, you ship goods you weren't paid for. GitHub webhook fakery: fake `release.published` event triggers your CI to deploy attacker-controlled artifacts. Clerk webhook fakery: fake `user.created` event seeds a malicious user into your database with admin role. The pattern is identical across providers — without signature verification, the webhook endpoint is an unauthenticated mutation API.
// what fixvibe checks
What FixVibe checks
FixVibe repo scans look for high-confidence security patterns and dependency risk in source context. Reports identify the affected area and recommended fix. For check-specific questions about exact detection heuristics, active payload details, or source-code rule patterns, contact support@fixvibe.app.
Ironclad defenses
Always verify signatures before parsing or trusting any body field. Use the official SDK helper when available — `stripe.webhooks.constructEvent(rawBody, sig, secret)` for Stripe, GitHub's documented HMAC SHA-256 over rawBody compared with `crypto.timingSafeEqual`, `svix.verify(rawBody, headers)` for Svix-shaped providers (Resend, Clerk, Linear). Critical: read the raw body before parsing — `req.text()` in Next.js, `bodyParser.raw()` in Express, `express.raw({ type: '*/*' })` middleware, or framework-specific raw-body access. Reject requests without signatures, even in test environments; sloppiness in test becomes sloppiness in prod. Validate the timestamp included in the signature (Stripe's tolerance is 5 minutes by default) so old captured events can't be replayed. Store webhook event IDs after processing and refuse re-processing the same ID — handles legitimate retries without enabling deliberate replay. As a final layer, scope the webhook secret per-provider per-environment so a leak is contained.
The takeaway
Signature verification is the difference between a webhook endpoint that's part of a trusted integration and one that's a public mutation API. The verification call is one line. Skipping it is the kind of bug you find in a postmortem rather than a code review.
