HookTestHookTest
← Back to Blog

Webhook Security Best Practices

Webhook endpoints are publicly accessible URLs that accept POST requests from external services. If you do not secure them properly, attackers can send fake events to your server — triggering unauthorized actions, corrupting data, or overwhelming your infrastructure.

Here are the practices that matter most.

1. Always Verify Signatures

Most webhook providers (Stripe, GitHub, Twilio, Shopify) sign payloads using HMAC or asymmetric keys. The signature is sent in a header, and you verify it using a shared secret. This is the single most important security measure.

Without signature verification, anyone who knows your endpoint URL can send crafted payloads. It is trivial to guess webhook URLs — they follow predictable patterns like /api/webhooks/stripe or /webhook.

Use the provider's SDK for verification when available. Rolling your own is error-prone. If you must verify manually, use constant-time comparison (crypto.timingSafeEqual in Node.js) to prevent timing attacks.

2. Use HTTPS Only

Webhook payloads travel over the public internet. Without TLS, the payload (and any secrets in headers) can be intercepted. Every webhook endpoint should use HTTPS. Most providers will not even let you register an HTTP URL in production.

If you are testing locally, use a tunneling tool or a service like HookTest to get an HTTPS URL that forwards to your local server.

3. Prevent Replay Attacks

A replay attack resends a previously valid webhook payload. Even with a valid signature, the event has already been processed. Two defenses:

  • Check timestamps. Many providers include a timestamp in the signature. Reject events older than 5 minutes. Stripe includes the timestamp in the Stripe-Signature header's t value.
  • Track processed event IDs. Store event IDs in your database and skip duplicates. This also protects against legitimate retries that arrive after you have already processed the event.

4. Return 200 Quickly

Webhook providers interpret slow responses as failures and will retry. If your handler takes 30 seconds to process, the provider sends the same event again while you are still working on the first one. This creates duplicate processing and wastes resources.

Acknowledge the webhook immediately with a 200 response, then process the payload asynchronously. Use a message queue (Redis, SQS, Bull) or a background job system. Your webhook handler should do three things: verify the signature, enqueue the event, and return 200.

5. Rate Limit Your Endpoint

Webhook endpoints are public URLs. Even with signature verification, an attacker can send thousands of requests with invalid signatures, consuming CPU on verification attempts. Apply rate limiting at the infrastructure level (nginx, Cloudflare, API gateway) before the request reaches your application code.

A reasonable limit depends on your provider. Stripe sends events in real time, so bursts during checkout rushes are normal. GitHub sends fewer events. Start with 100 requests per minute per IP and adjust based on your traffic patterns.

6. Keep Secrets Out of URLs

Some developers add a "secret token" as a query parameter in the webhook URL: /webhook?token=abc123. This is better than nothing but worse than proper signature verification. URL parameters end up in server logs, browser history, and referrer headers. Use header-based authentication instead.

Testing Your Security

Before going to production, test that your endpoint correctly rejects tampered payloads, expired timestamps, and missing signatures. Send requests with modified bodies and invalid signatures — your handler should return 400 or 401, not 200.

HookTest is useful here too. Capture a real webhook payload, then replay it with modifications to verify your verification logic catches the tampering.