Webhook Security Testing: SSRF, Replay Attacks, and Signature Bypass
Webhooks are everywhere. Payment processors, CI/CD platforms, SaaS integrations — virtually every modern application either sends or receives webhooks. From a security perspective, webhooks represent two distinct attack surfaces: the inbound consumer (your endpoint that receives events from a third party) and the outbound sender (your system that pushes events to a user-configured URL). Both sides have critical vulnerabilities that are frequently overlooked.
This guide covers the full webhook attack surface — what breaks, how to test it, and what secure implementations look like.
Webhook Architecture: Two Attack Surfaces
Before testing, understand which side of the webhook you're dealing with:
| Side | Role | Primary Risks |
|---|---|---|
| Outbound (sender) | Your app pushes events to a user-registered URL | SSRF via webhook URL, secrets in payloads, no delivery retries |
| Inbound (consumer) | Your endpoint receives events from GitHub, Stripe, etc. | Signature bypass, replay attacks, event injection, DoS |
Most applications have both. A SaaS platform might let customers register webhook URLs (outbound) while also consuming webhooks from Stripe for payment events (inbound).
Outbound Webhook Vulnerabilities
SSRF via User-Controlled Webhook URL
When users register a webhook URL in your application and the server fetches that URL to deliver events, you have a classic Server-Side Request Forgery (SSRF) vector. The server makes an outbound HTTP request to an attacker-controlled URL — which can be pointed at internal infrastructure.
Test cases:
- Register
http://169.254.169.254/latest/meta-data/iam/security-credentials/(AWS IMDS v1) - Register
http://metadata.google.internal/computeMetadata/v1/(GCP metadata) - Register
http://169.254.169.254/metadata/instance?api-version=2021-02-01(Azure IMDS) - Register
http://localhost:6379/(Redis),http://127.0.0.1:9200/(Elasticsearch) - Register
http://10.0.0.1/,http://192.168.1.1/(private IP ranges) - Use DNS rebinding: register a domain that initially resolves to a public IP but rebinds to a private IP after the validation check
- Test URL schemes:
file://,dict://,gopher:// - Test URL encoding bypasses:
http://127.1/,http://0x7f000001/,http://2130706433/
Secure implementation: Validate the URL against an allowlist of schemes (https only), resolve DNS before making the request and reject private IP ranges, and use a dedicated outbound network segment without internal access.
Secrets Leakage in Webhook Payloads
Webhook payloads often contain sensitive event data. Test whether your outbound webhooks include more data than necessary — PII, internal IDs, or fields that shouldn't be shared with external consumers.
- Register a webhook with a capture endpoint (webhook.site, requestbin)
- Trigger events and inspect every payload field
- Check for internal user IDs, session tokens, PII (email, phone, address)
- Check if error webhooks include stack traces or internal paths
Missing Delivery Confirmation
Applications that don't verify webhook delivery can be exploited for event injection — an attacker can replay or forge events if the receiver can't distinguish legitimate deliveries from attacker-crafted requests.
Inbound Webhook Vulnerabilities
Signature Validation Bypass
Most webhook providers (GitHub, Stripe, Twilio) sign payloads using HMAC-SHA256. Your endpoint is supposed to verify this signature before processing the event. Failure to validate — or validating incorrectly — allows event injection.
Common bypass patterns:
== short-circuits on the first mismatched byte, leaking information about correct signature bytes. Always use hmac.compare_digest() (Python) or equivalent.
sha256=<hash>. Application extracts everything after = and validates but doesn't check the prefix. Attacker sends sha1=<weak-hash> and the application accepts it if it also validates the body against SHA-1.
Test approach:
# Send event with no signature header
curl -X POST https://yourapp.com/webhooks/github \
-H "Content-Type: application/json" \
-d '{"action": "push", "repository": {"full_name": "attacker/repo"}}'
# Send event with wrong signature
curl -X POST https://yourapp.com/webhooks/github \
-H "X-Hub-Signature-256: sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
-H "Content-Type: application/json" \
-d '{"action": "push"}'
# Test algorithm substitution
curl -X POST https://yourapp.com/webhooks/github \
-H "X-Hub-Signature: sha1=da39a3ee5e6b4b0d3255bfef95601890afd80709" \
-H "Content-Type: application/json" \
-d '{}'
Replay Attack
Without a timestamp check and nonce validation, valid webhook payloads can be captured and replayed indefinitely. A legitimate payment event could be replayed to trigger duplicate processing — duplicate orders, double credit, or repeated provisioning actions.
Test: Capture a legitimate webhook delivery (from a test environment). Re-send the exact same payload and headers — including the valid signature — hours later. If it's processed, replay is possible.
Secure implementation:
- Require a timestamp header (e.g.,
X-Webhook-Timestamp) signed as part of the HMAC - Reject events where the timestamp is older than 5 minutes
- Store and deduplicate event IDs (idempotency keys) to prevent replay within the timestamp window
- Stripe's model: HMAC is computed over
timestamp.payload; timestamp is verified to be within 300 seconds
# Stripe-style signature verification (Python)
import hmac, hashlib, time
def verify_stripe_signature(payload_body, sig_header, secret, tolerance=300):
parts = dict(p.split('=', 1) for p in sig_header.split(','))
timestamp = int(parts['t'])
signatures = [v for k, v in parts.items() if k == 'v1']
# Reject stale events
if abs(time.time() - timestamp) > tolerance:
raise ValueError("Timestamp outside tolerance window")
signed_payload = f"{timestamp}.{payload_body.decode()}"
expected = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
if not any(hmac.compare_digest(expected, sig) for sig in signatures):
raise ValueError("Signature mismatch")
Event Injection via Forged Payloads
If signature validation is missing or bypassable, attackers can send crafted events to trigger application logic. Common targets:
- Payment webhooks: Forge a
payment.succeededevent to provision services without actual payment - CI/CD webhooks: Forge a push event to trigger builds on arbitrary code
- User lifecycle events: Forge an account upgrade or role change event
- Deletion events: Forge a
user.deletedevent to trigger account cleanup for active users
Webhook Event Type Confusion
Applications that trust the event.type field in the payload without verifying it against what the endpoint expects can be tricked into processing the wrong event type.
Test: Send a webhook payload with a mismatched event type. For example, send a charge.refunded payload to an endpoint that's only supposed to handle charge.succeeded events. If it processes the event anyway, the application isn't verifying event type against expected type.
DoS via Webhook Flooding
Webhook endpoints are often not rate-limited. An attacker with a valid sender account can fire thousands of events per second, overwhelming the endpoint or triggering expensive downstream processing.
- Test whether the endpoint has rate limiting per source IP or per webhook source
- Check if processing is synchronous (blocking) or queued — synchronous processing is vulnerable to slowloris-style attacks
- Test with large payload bodies — many webhook implementations don't enforce body size limits
Webhook Registration Security
Applications that allow users to register webhook URLs need additional controls:
| Control | Why It Matters | How to Test |
|---|---|---|
| URL validation | Prevents SSRF | Register private IPs, localhost, cloud metadata endpoints |
| HTTPS enforcement | Prevents payload interception in transit | Register http:// URL and verify it's rejected |
| URL ownership verification | Prevents webhooks being sent to third-party URLs | Register a URL you don't control; check if app verifies you own it |
| Secret rotation | Limits exposure after credential leak | Verify users can rotate the signing secret without losing history |
| Tenant isolation | Prevents tenant A from receiving tenant B's events | Check if webhook events are scoped to the correct tenant context |
Testing Webhook Security with Burp Suite
# 1. Intercept outbound webhook requests
# Set up Burp as upstream proxy for your test environment
# Trigger webhook events and observe the outbound request
# 2. Test signature bypass
# Capture a legitimate webhook delivery in Burp HTTP history
# Modify the payload body and resend with original signature
# If accepted: signature is not validated or validated incorrectly
# 3. Test replay (using Burp Repeater)
# Capture a valid webhook delivery
# Wait 10+ minutes, then replay from Repeater
# If accepted: no timestamp validation
# 4. SSRF via webhook URL (using Burp Collaborator)
# Register a Burp Collaborator URL as a webhook endpoint
# Trigger delivery — observe DNS/HTTP interactions in Collaborator
# Pivot to internal IPs once SSRF is confirmed
Webhook Security Testing Checklist
- Verify HMAC signature validation is present on all inbound webhook endpoints
- Verify signatures are compared with constant-time comparison (not
==) - Verify the raw request body (not re-serialized JSON) is used for HMAC computation
- Verify timestamp validation rejects events older than 5 minutes
- Verify event ID deduplication prevents replay within the timestamp window
- Test for SSRF by registering internal/metadata URLs as webhook destinations
- Verify HTTPS is enforced for webhook destination URLs
- Test for event type confusion — send mismatched event types to type-specific endpoints
- Test for DoS via flooding and large payloads
- Verify webhook payloads don't contain unnecessary sensitive data
- Check tenant isolation — events must be scoped to the correct tenant
- Verify webhook secrets are rotatable without service disruption
Ironimo scans web applications with Kali Linux-grade tooling, including SSRF detection via webhook URL injection and API security testing. Run a scan without writing a custom test script.
Start free scan