Back to Blog
securityauditbest practicesproduction

Webhook Security Checklist: 12 Things to Audit Before Going to Production

Webhook endpoints are HTTP endpoints with real-world consequences — a single vulnerability can lead to unauthorized charges, data exfiltration, or complete account takeover. Here's a security audit checklist for both inbound and outbound webhook infrastructure.

N
Nadia Kowalski
Security Engineer
February 19, 2026
10 min read

Webhook endpoints are among the most commonly misconfigured parts of modern web infrastructure. They sit at the intersection of "must be publicly accessible" and "triggers real-world consequences" — a difficult security position.

An unsecured inbound webhook can allow an attacker to:

  • Trigger payment processing with fake events
  • Mark orders as fulfilled without actual payment
  • Provision unauthorized accounts
  • Inject malicious payloads into your data pipeline

This checklist covers 12 security controls to audit before your webhook infrastructure goes to production.


Inbound Security (Receiving Webhooks)

1. Signature Verification is Enforced on Every Request

What to check: Every request to your inbound webhook endpoint must have its signature verified before any processing occurs. There should be no code path that processes a webhook payload without first verifying the HMAC signature.

Common failure mode: Signature verification exists but is gated behind an if verifySignatures flag that defaults to false in development and was never set to true in production.

Audit:

bash
# Search for any code path that might skip verification
grep -r "skip.*signature\|signature.*skip\|verify.*false" ./handlers/

Fix: Make signature verification unconditional. No feature flag, no environment bypass.


2. Constant-Time Signature Comparison

What to check: HMAC comparisons must use constant-time equality functions, not string equality operators.

Why it matters: Standard string comparison (==) short-circuits on the first differing byte. An attacker can use timing measurements to brute-force valid signatures byte by byte — this is a practical attack, not a theoretical one.

Vulnerable code:

go
// ❌ Timing side-channel
if computedHMAC == receivedHMAC {

Secure code:

go
// ✅ Constant-time comparison
if !hmac.Equal([]byte(computedHMAC), []byte(receivedHMAC)) {

In Python: hmac.compare_digest. In Node.js: crypto.timingSafeEqual. In Ruby: ActiveSupport::SecurityUtils.secure_compare.


3. Timestamp Validation (Replay Attack Prevention)

What to check: If the webhook signature includes a timestamp (Stripe's format: t=<unix>,v1=<hex>), validate that the timestamp is recent. Reject events older than 5 minutes.

Why it matters: Without timestamp validation, an attacker who obtains a valid signed payload can replay it hours or days later.

Implementation:

go
const maxTimestampAge = 5 * time.Minute

func validateTimestamp(timestamp string) bool {
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil {
        return false
    }
    age := time.Since(time.Unix(ts, 0))
    return age >= 0 && age <= maxTimestampAge
}

If the provider doesn't include a timestamp, use X-Request-Timestamp from a gateway that adds one (like GetHook).


4. Payload Size Limits

What to check: Your webhook endpoint must reject payloads above a reasonable size limit before reading them into memory.

Why it matters: Without a size limit, an attacker can send a multi-gigabyte payload that exhausts memory and OOM-kills your process.

Implementation:

go
http.MaxBytesReader(w, r.Body, 10*1024*1024) // 10MB limit

Apply this before any io.ReadAll call. MaxBytesReader returns an error if the body exceeds the limit.

Typical webhook payload size: 1KB–100KB. A 10MB limit is conservative enough to block attacks while allowing legitimate large payloads.


5. Secrets Not Stored in Plaintext

What to check: Webhook signing secrets stored in your database must be encrypted at rest. Plaintext secrets in the database means a SQL injection vulnerability becomes a full secret compromise.

Audit:

sql
-- If this returns rows with visible secret values, you have a problem
SELECT source_id, verification_config FROM sources LIMIT 5;

Fix: Use AES-256-GCM encryption for all secret fields. GetHook encrypts all secrets at rest — the database stores ciphertext, never plaintext.


6. No Raw Secrets in Logs

What to check: Webhook payloads, HMAC signatures, and API keys must never appear in application logs.

Audit:

bash
# Check logs for potential secret patterns
grep -E "hk_[a-zA-Z0-9]+" production.log
grep -E "sha256=[a-f0-9]{64}" production.log
grep -E "Bearer [a-zA-Z0-9]+" production.log

Common failure mode: Debug logging during development that logs the full request (including Authorization: Bearer ... headers) is never removed before production.

Fix: Log request metadata (method, path, status, latency), not request content. If you must log headers, explicitly redact sensitive ones.


Outbound Security (Sending Webhooks)

7. Signing Every Outbound Event

What to check: Every webhook you send to customers must include an HMAC signature so recipients can verify authenticity. The signature should use a per-destination secret, not a shared global key.

Why per-destination? If you use one global signing key and one customer's system is compromised, the attacker learns a key that validates webhooks for all your customers.

Implementation:

go
signature := computeHMAC(payload, destination.SigningSecret)
req.Header.Set("X-Webhook-Signature", "t="+timestamp+",v1="+signature)

8. HTTPS-Only Destinations

What to check: Reject webhook delivery to http:// destinations in production. HTTP webhooks expose payload content and signatures to network-level attackers.

Why it matters: An attacker on the same network segment can see the raw HMAC signature. Since the signature includes the payload, they can verify that a payload was sent — and if you reuse secrets, they can craft forged events.

Implementation:

go
if !strings.HasPrefix(destination.URL, "https://") {
    return errors.New("destination URL must use HTTPS")
}

Allow an override for development environments only, gated behind an ALLOW_HTTP_DESTINATIONS=true env var that's never set in production.


9. Destination URL Validation (SSRF Prevention)

What to check: Before delivering to a user-supplied destination URL, validate that it doesn't point to internal network resources.

Why it matters: If a customer can configure http://169.254.169.254/latest/meta-data/ (AWS instance metadata) as their destination, your delivery worker will fetch and return AWS credentials in the delivery attempt log. This is Server-Side Request Forgery (SSRF).

Blocked destination patterns:

  • localhost, 127.0.0.1, ::1
  • 169.254.169.254 (cloud metadata)
  • 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC 1918 private ranges)
  • 0.0.0.0
  • Custom internal domains (e.g., *.internal.yourcompany.com)
go
func isAllowedDestination(rawURL string) bool {
    u, err := url.Parse(rawURL)
    if err != nil {
        return false
    }

    host := u.Hostname()
    ip := net.ParseIP(host)

    if ip != nil {
        return !isPrivateIP(ip)
    }

    // Resolve hostname and check resolved IPs
    addrs, err := net.LookupHost(host)
    if err != nil {
        return false
    }

    for _, addr := range addrs {
        if isPrivateIP(net.ParseIP(addr)) {
            return false
        }
    }

    return true
}

Note: DNS rebinding attacks can bypass hostname checks. After resolving the hostname, use the resolved IP for the actual HTTP request (pin the connection to the resolved IP).


10. Secret Rotation Without Downtime

What to check: Your system must support rotating webhook signing secrets without dropping events. The standard pattern is dual-signature support with a grace period.

Why it matters: Signing secrets occasionally need rotation — due to suspected compromise, regular security policy, or customer request. Without a rotation mechanism, rotating a secret immediately breaks all in-flight webhooks.

Dual-signature pattern:

Step 1: Add new_secret alongside old_secret in destination config
Step 2: Sign deliveries with both secrets during transition period
Step 3: Recipients accept events signed by either secret
Step 4: After 24h grace period, remove old_secret

GetHook's destination config supports a signing secret version field for this rotation flow.


11. Delivery Attempt Logs Don't Store Response Bodies Verbatim

What to check: When recording delivery attempt outcomes, be careful about what you store from the destination's HTTP response body.

Why it matters: If a destination responds with error details that include stack traces, internal error messages, or (worst case) sensitive data from the destination's system, storing these verbatim creates a data handling issue.

Safe approach:

  • Store response status code and a truncated excerpt (first 500 bytes) of the response body
  • Mark the response body with a content type classification (JSON, HTML, text)
  • Apply data retention policies to delivery logs separately from event data

12. Tenant-Scoped API Key Authorization

What to check: Every management API call (listing events, replaying events, updating destinations) must be authorized to the owning tenant's account. There must be no endpoint that returns data across tenant boundaries.

Audit test:

bash
# Get API key for Tenant A
TOKEN_A="hk_..."

# Try to access Tenant B's events using Tenant A's key
curl -H "Authorization: Bearer $TOKEN_A" \
  https://api.yoursaas.com/v1/events/evt_TENANT_B_EVENT_ID

# Should return 404 (not 403, which confirms the event exists)

Return 404, not 403, for cross-tenant access. Returning 403 confirms that the resource exists, which leaks information.


Security Audit Checklist Summary

Inbound:

  • Signature verification on every request, no bypasses
  • Constant-time HMAC comparison
  • Timestamp validation (reject events > 5 minutes old)
  • Payload size limit enforced
  • Secrets encrypted at rest
  • No secrets in application logs

Outbound:

  • Every delivery signed with per-destination secret
  • HTTPS-only destination enforcement
  • SSRF prevention on user-supplied URLs
  • Secret rotation without downtime support
  • Response body storage policy defined
  • Cross-tenant authorization enforced (404 for cross-tenant)

Running this audit before your first production customer will catch the issues that are expensive to fix later. See GetHook's security architecture →

Stop losing webhook events.

GetHook gives you reliable delivery, automatic retry, and full observability — in minutes.