Webhook signature verification — Stripe, GitHub, Clerk, everyone
If your webhook endpoint skips signature verification, an attacker can trigger any downstream action you code — refunds, subscription changes, user upgrades. Here is how to verify signatures correctly for the five most common webhook providers.
Webhook endpoints receive unsolicited POSTs from external services. If your handler takes action without verifying the request actually came from the stated provider, any attacker who finds the URL can forge events.
What it is
Every legitimate webhook provider signs outbound requests with a secret shared only with your app. Signature verification on the receiving side confirms the payload was produced by that provider and has not been tampered with in transit.
Vulnerable example
// Vulnerable: no signature check. Any attacker who finds this URL can forge events.
export async function POST(req: Request) {
const event = await req.json();
if (event.type === "invoice.paid") {
await grantAccess(event.data.customer);
}
return Response.json({ received: true });
}Fixed example
// Fixed: Stripe signature verification
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const sig = req.headers.get("stripe-signature");
const body = await req.text(); // raw body is required for signature check
let event;
try {
event = stripe.webhooks.constructEvent(
body,
sig!,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch {
return new Response("invalid signature", { status: 400 });
}
if (event.type === "invoice.paid") {
await grantAccess((event.data.object as { customer: string }).customer);
}
return Response.json({ received: true });
}How Securie catches it
Securie's webhook specialist detects every POST handler that receives JSON from an external source and flags missing signature-verification. The fix includes the exact library snippet for the matched provider (Stripe, GitHub, Clerk, Svix, Resend, Vercel, Linear, Notion, and more).
Checklist
- Every webhook endpoint verifies the provider's signature
- Raw request body is preserved for HMAC computation (not JSON-parsed first)
- Webhook secrets are per-environment and rotated on team departures
- Constant-time comparison is used for signature matching
- Invalid signatures return 400, not 200 (prevents retry leakage)
- Replay-attack protection via timestamp window
FAQ
Can I just allowlist the provider's IP range?
No. IP allowlists break when providers rotate their CDN, and anyone who routes through that CDN (including other customers) appears to come from an allowlisted IP. Use signatures.
What about webhooks from my own systems?
Still sign them. Internal-vs-external is a brittle trust boundary; an attacker inside the same VPC can forge unsigned internal webhooks.