Webhook Security Checklist — HMAC Signature Validator & Security Audit

May 25, 2026 · 13 min read · By Michael Lip

Webhook endpoints are attack surfaces. Every publicly reachable URL that accepts POST requests and triggers actions in your system is a target for payload injection, replay attacks, and denial of service. The difference between a secure webhook endpoint and a vulnerable one comes down to a checklist of specific, verifiable security measures — signature verification, timestamp validation, IP allowlisting, rate limiting, and proper error handling.

This page provides two interactive tools. The HMAC Signature Validator lets you compute and verify webhook signatures using the same cryptographic primitives that providers like Stripe and GitHub use. The Security Audit Checklist is an 18-item assessment covering every layer of webhook security, from transport encryption to application-level validation. Check off what you have implemented, and the tool calculates a security score with a letter grade.

HMAC Signature Validator
Click "Compute" to generate the HMAC signature.
Enter an expected signature above to verify.
Webhook Security Audit Checklist
0%
F
Complete the checklist below
0 / 18 items passed
Transport Security
Signature Verification
Replay & Duplication Protection
Access Control
Error Handling & Monitoring

Why Webhook Security Matters

Webhook endpoints are inherently dangerous because they accept external input and trigger internal actions. A payment webhook might credit a user's account. A deployment webhook might trigger a production release. A notification webhook might send emails or SMS messages. If an attacker can forge a valid webhook request, they can trigger any of these actions without authorization.

The attack surface is wide. Webhook URLs are often discoverable through configuration leaks, error messages, or brute force. Once an attacker knows the URL, they can send arbitrary POST requests with crafted payloads. Without signature verification, the endpoint has no way to distinguish a legitimate webhook from a forged one. Without timestamp validation, an attacker who intercepts a valid signed request can replay it indefinitely. Without rate limiting, an attacker can flood the endpoint with requests to cause denial of service or trigger resource exhaustion.

The security measures in this checklist are not optional best practices — they are the minimum requirements for operating a webhook endpoint in production. Skipping any of the critical items creates a direct path to exploitation.

HMAC Signature Verification in Detail

HMAC (Hash-based Message Authentication Code) provides two guarantees: authenticity (the message was sent by someone who knows the secret) and integrity (the message was not modified in transit). Both guarantees are achieved through a single cryptographic operation — hashing the message with the shared secret key.

The verification process works as follows. The webhook provider computes HMAC-SHA256(secret, raw_body) and includes the result in a request header. Your endpoint performs the same computation on the received body using the same secret. If the two values match, the payload is authentic and unmodified. If they differ, the payload was either sent by someone who does not know the secret or was modified after signing.

The critical implementation detail is using the raw request body for the HMAC computation. When a web framework parses the JSON body, it may reorder keys, modify whitespace, or change number formatting. Re-serializing the parsed object produces a different byte sequence, which produces a different hash. In Express.js, capture the raw body with express.raw({ type: 'application/json' }). In Python Flask, use request.get_data(). In Go, read from r.Body before any JSON decoding.

Constant-time comparison is the second critical detail. Standard string comparison (=== in JavaScript, == in Python) returns false at the first differing byte, which means the comparison takes less time for strings that differ early. An attacker can exploit this timing difference to discover the correct signature byte by byte. Use crypto.timingSafeEqual() in Node.js, hmac.compare_digest() in Python, or subtle.ConstantTimeCompare() in Go.

Timestamp Validation and Replay Prevention

Even with valid HMAC signatures, webhook requests can be replayed. If an attacker intercepts a legitimately signed request (through network sniffing, log access, or compromised middleware), they can resend it at any time. The signature is still valid because the payload has not changed. Timestamp validation prevents this attack by including the current time in the signed data and rejecting requests that are too old.

Stripe implements this elegantly. The Stripe-Signature header contains t=timestamp,v1=signature. The timestamp is included in the HMAC computation: HMAC-SHA256(secret, timestamp + "." + body). Your endpoint checks that the timestamp is within 5 minutes of the current time. If an attacker replays the request 6 minutes later, it is rejected even though the signature is technically valid.

For providers that do not include a timestamp in the signature, implement idempotency-based replay prevention instead. Store every processed event ID in a cache (Redis, database, or in-memory store) with a TTL matching the provider's maximum retry window. Before processing any event, check if the ID has already been processed. This approach handles both malicious replays and legitimate retries from the provider.

IP Allowlisting as Defense in Depth

IP allowlisting restricts your webhook endpoint to accept requests only from known provider IP ranges. GitHub publishes their webhook source IPs via the /meta API endpoint. Stripe publishes a list in their documentation. Shopify provides webhook source IPs per region. By configuring your firewall or application to reject requests from other IPs, you block spoofed requests before they reach your application code.

IP allowlisting should never be your only security measure. IP addresses can be spoofed (though it is difficult for TCP connections), provider IP ranges change over time, and requests may traverse proxies or load balancers that modify the source IP. Treat IP allowlisting as an additional layer that reduces the attack surface but does not replace signature verification.

Rate Limiting and Resource Protection

Webhook endpoints must be rate-limited to prevent abuse. Even with signature verification, an attacker who discovers your webhook secret (through a code leak, credential stuffing, or a compromised provider account) could flood your endpoint with valid requests. Rate limiting ensures that even in the worst case, the damage is bounded.

Set rate limits based on your expected webhook volume plus a reasonable buffer. If you typically receive 100 webhooks per minute from Stripe, a rate limit of 500 per minute per IP gives enough headroom for bursts while preventing unbounded abuse. Return a 429 Too Many Requests response when the limit is exceeded — the provider will retry later.

Async Processing and Response Time

Webhook endpoints should return a 200 OK response as quickly as possible — ideally within 1-2 seconds, and no more than the provider's timeout (typically 5-30 seconds). If your endpoint takes too long to respond, the provider will consider the delivery failed and schedule a retry, leading to duplicate processing.

The standard pattern is to validate the request (check signature, verify timestamp, check for duplicates), enqueue the event for async processing, and return 200 immediately. The actual business logic runs asynchronously from a queue (SQS, RabbitMQ, BullMQ, Celery). This separation ensures fast responses and allows retry logic for the processing step to be independent of the webhook delivery retry logic.

Error Handling Without Information Leakage

Your webhook endpoint should return minimal information in error responses. A 401 Unauthorized for invalid signatures, 400 Bad Request for malformed payloads, and 500 Internal Server Error for processing failures. Never include stack traces, database errors, file paths, or internal identifiers in the response body. An attacker probing your endpoint will use these details to refine their attacks. The provider only needs the status code to determine whether to retry. Teams implementing comprehensive security monitoring should log full error details internally while returning sanitized responses to callers. Developers working with webhook payload formats can use the payload builder to test their error handling paths.

Frequently Asked Questions

How do I verify a webhook signature with HMAC-SHA256?

Compute HMAC-SHA256 of the raw request body using your webhook secret, then compare the hex digest to the signature header. In Node.js: const sig = crypto.createHmac('sha256', secret).update(rawBody).digest('hex'). Use crypto.timingSafeEqual() for comparison to prevent timing attacks. Always hash the raw bytes, never parsed JSON.

What are the most common webhook security vulnerabilities?

The top vulnerabilities are: missing signature verification (accepting any request), using parsed JSON instead of raw body for HMAC (breaks signatures), string comparison instead of constant-time comparison (enables timing attacks), no timestamp validation (allows replay attacks), missing TLS (payloads visible in transit), and overly permissive IP allowlists.

Should I restrict webhook endpoints by IP address?

IP allowlisting is a defense-in-depth measure but should not be your only protection. Major providers publish their webhook source IPs. IP allowlisting blocks spoofed requests before they reach your application code, but HMAC signature verification is still required because IPs can be spoofed at the network level.

How do I prevent webhook replay attacks?

Include a timestamp in the signed payload and reject requests older than 5 minutes. Store processed event IDs and reject duplicates. Together, timestamp validation and idempotency keys prevent both replay attacks and duplicate processing from legitimate retries.

What should my webhook endpoint return on failure?

Return 200 for successfully processed events, 202 for accepted-but-queued events, 400 for malformed payloads, 401 for invalid signatures, and 500 for internal processing errors. Never return detailed error messages — they leak implementation details to attackers. The status code alone tells the provider whether to retry.

Related Tools