Webhook Security Best Practices

Protect your webhook endpoints from the 5 most common attacks with code examples and a manual checklist.

Why Webhook Security Matters

Webhooks are powerful, but they expose your server to incoming HTTP traffic from the internet. A misconfigured webhook endpoint can lead to:

The good news: every one of these attacks is preventable. Review the security checklist

The 5 Biggest Webhook Security Risks

Critical 1. Replay Attacks

An attacker captures a legitimate webhook request and re-sends it minutes, hours, or days later. Without timestamp validation, your system processes it again — double-charging customers, duplicating orders, or re-triggering deployments.

Defense: Check the timestamp header (e.g., Stripe's t= field). Reject any request older than 5 minutes. Store processed event IDs and skip duplicates.

Critical 2. Man-in-the-Middle (MITM) Attacks

If your endpoint accepts plain HTTP, anyone on the network path can read and modify webhook payloads. Payment amounts, customer emails, and API keys in transit are all exposed.

Defense: Only accept HTTPS. Redirect HTTP to HTTPS. Use HSTS headers. Verify your TLS certificate is valid and not expired.

Critical 3. Webhook Spoofing

An attacker sends a fake webhook to your endpoint, pretending to be Stripe, GitHub, or any other provider. Without signature verification, your system trusts and processes the forged payload.

Defense: Verify the HMAC signature on every request. Never skip verification, even in development. See code examples below.

High 4. Server-Side Request Forgery (SSRF)

If your system allows users to configure webhook destination URLs, an attacker can point them to internal services like http://169.254.169.254 (cloud metadata) or http://localhost:6379 (Redis). This can leak secrets and compromise your infrastructure.

Defense: Validate destination URLs. Block private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 169.254.0.0/16). Resolve DNS and check the IP before connecting. Block DNS rebinding.

High 5. Data Leaks via Logging

Webhook payloads often contain PII, payment details, or access tokens. If you log full request bodies to stdout, error trackers, or monitoring tools, this sensitive data ends up in places it shouldn't.

Defense: Redact sensitive fields before logging. Never log full webhook bodies in production. Use structured logging with field-level controls.

Audit these risks manually

How to Verify Webhook Signatures

Signature verification is the single most important webhook security measure. Here's how to implement it for the two most popular languages:

Node.js (Express)

JavaScript
const crypto = require('crypto');

function verifyWebhookSignature(req, secret) {
  const signature = req.headers['x-webhook-signature'];
  const timestamp  = req.headers['x-webhook-timestamp'];

  // 1. Reject old timestamps (prevent replay attacks)
  const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
  if (age > 300) throw new Error('Timestamp too old');

  // 2. Compute expected signature
  const payload  = timestamp + '.' + req.body;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  // 3. Constant-time comparison (prevents timing attacks)
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )) {
    throw new Error('Invalid signature');
  }

  return true;
}

Python (Flask)

Python
import hmac, hashlib, time

def verify_webhook(request, secret):
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')

    # 1. Reject old timestamps (prevent replay attacks)
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError('Timestamp too old')

    # 2. Compute expected signature
    payload = f'{timestamp}.{request.data.decode()}'
    expected = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # 3. Constant-time comparison (prevents timing attacks)
    if not hmac.compare_digest(signature, expected):
        raise ValueError('Invalid signature')

    return True

HTTPS Enforcement

Every webhook endpoint must use HTTPS. Here's a quick checklist:

Most providers (Stripe, GitHub, Shopify) refuse to send webhooks to HTTP URLs. But if yours doesn't enforce this, you must.

IP Allowlisting

Many webhook providers publish the IP addresses they send from. Configure your firewall or reverse proxy to accept webhook traffic only from these ranges:

Best Practice

Combine IP allowlisting with signature verification. IP-based filtering alone is not enough — IPs can be spoofed or change without notice. Defense in depth.

Rate Limiting

Even with signature verification, you should rate-limit your webhook endpoints:

Best Practice

Return 200 OK immediately and process the webhook in a background queue. If your handler takes more than 5 seconds, most providers will retry, creating a thundering herd.

Security Checklist

Use this checklist to audit your webhook endpoints:

Work through each item in staging and record the request, response, and configuration evidence for your review.

Review Your Webhook Security

Use the checklist above to verify signatures, replay handling, TLS, rate limits, SSRF controls, and log redaction.

Open the Checklist What is a Webhook?

Related guides: What is a Webhook? · Webhook vs API · Retry Strategies

More: What is a Webhook? | All Features | Stripe Webhook Guide | Compare Tools

Start catching webhooksTry Free →