Webhook Security Best Practices

Why Webhook Security Matters

Webhook endpoints are publicly accessible URLs that accept incoming HTTP requests. Anyone who knows (or guesses) your endpoint URL can send fake requests pretending to be a legitimate webhook provider. Without proper security measures, an attacker could send a fake "payment succeeded" event and trick your application into granting access or fulfilling orders without actual payment.

The attack surface is significant. Webhook URLs often follow predictable patterns (/api/webhooks/stripe, /webhooks/github), and they accept POST requests by design. Unlike API endpoints that your application calls (where you control the request), webhook endpoints accept unsolicited incoming requests from external sources.

Implementing proper webhook security is not optional — it is essential for any production application that processes webhook events. The good news is that most webhook providers include security features, and implementing them correctly is straightforward.

Verify Webhook Signatures

The most important security measure is verifying webhook signatures. Most providers (Stripe, GitHub, Shopify, Slack, and others) sign each webhook request using a shared secret. Your handler should verify this signature before processing the event.

The typical flow is: the provider computes an HMAC hash of the request body using a secret key, and includes the hash in a header. Your handler computes the same hash using the same secret key and compares it with the header value. If they match, the request is authentic.

javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) { const computed = crypto .createHmac('sha256', secret) .update(payload) .digest('hex');

return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(computed) ); } ```

Always use crypto.timingSafeEqual for comparing signatures. A regular string comparison (===) is vulnerable to timing attacks where an attacker can deduce the correct hash one character at a time by measuring response times.

Prevent Replay Attacks

A replay attack occurs when an attacker captures a legitimate webhook request and re-sends it to your endpoint. Even with signature verification, the replayed request has a valid signature because it was originally signed by the provider.

Protect against replay attacks by checking the timestamp included in the webhook. Most providers include a timestamp in the signature computation or as a separate header. Reject requests where the timestamp is more than a few minutes old:

javascript
function isTimestampValid(timestamp, toleranceSeconds = 300) {
  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - timestamp) < toleranceSeconds;
}

Additionally, store processed event IDs and reject duplicates. This prevents both replay attacks and accidental duplicate deliveries. Use the provider's unique event ID (like Stripe's evt_ ID or GitHub's delivery ID) as the deduplication key.

During development, ReqPour's replay feature intentionally re-sends requests — this is useful for testing but remember that your production handler should reject replayed events.

Use HTTPS and Validate Sources

Always use HTTPS for your webhook endpoint URL. This encrypts the request in transit, preventing man-in-the-middle attacks from reading or modifying webhook payloads. Most providers require HTTPS endpoints, and ReqPour provides HTTPS URLs by default.

Some providers publish the IP addresses or IP ranges their webhook servers use. If available, you can add IP allowlisting as an additional security layer — reject requests from IP addresses outside the provider's known ranges. However, do not rely on this as your only security measure, since IP addresses can change.

Set your webhook endpoint to accept only POST requests. Reject GET, PUT, DELETE, and other methods. This reduces the attack surface and prevents accidental data exposure through GET parameters.

Secure Error Handling and Logging

Do not leak internal error details in webhook responses. If signature verification fails, return a generic 401 response — do not tell the requester which part of the verification failed. Detailed error messages help attackers refine their approach.

Log all webhook receipts, including failed signature verifications. These logs are invaluable for security monitoring and incident response. Include the IP address, headers, and a hash of the body (not the full body, which might contain sensitive data) in your security logs.

Rate-limit your webhook endpoint to prevent denial-of-service attacks. An attacker could flood your endpoint with fake requests, consuming server resources even if each request fails signature verification. Apply rate limits at the infrastructure level (reverse proxy or WAF) rather than in your application code.

During development, use ReqPour to inspect the security headers of incoming webhooks. Understanding exactly what headers each provider sends helps you implement verification correctly before deploying to production.

Get started with ReqPour

Catch, inspect, and relay webhooks to localhost. Free to start, $3/mo for Pro.