All Glossary Terms
Security

What is a replay attack?

A replay attack is when an attacker captures a valid signed webhook and re-sends it later to trigger duplicate or stale processing. The attacker doesn't forge anything — they intercept a real signed delivery (via network sniffing, log file access, or a compromised proxy) and re-send it hours, days, or months later. Signature verification still passes because the signature was valid at the time. The two-layer defense: a timestamp inside the signed payload that the receiver checks against a tolerance window (Stripe recommends 5 minutes), plus event-ID-keyed idempotency that returns 200 immediately on duplicates without re-running business logic.

A replay attack works even though the signature is valid. The attacker doesn't forge anything — they capture a real, signed webhook delivery (via network sniffing, log file access, a compromised intermediate proxy, or any other interception path) and re-send it to your endpoint hours, days, or months later. Your signature verification passes because the signature *was* valid. Your handler runs the business logic again.

What this lets the attacker do depends on your business logic. If the webhook is payment.refunded, replaying it issues a duplicate refund (idempotency at the source means Stripe doesn't actually refund twice, but your handler may still update local state, send notifications, etc.). If the webhook is subscription.canceled and your handler revokes access, replaying it revokes access for the same user a second time — annoying, sometimes recoverable, sometimes not.

The two-layer defense is timestamp tolerance plus idempotency. Most webhook signature schemes (Stripe, Slack) include a Unix timestamp inside the signed payload — t=1697045123,v1=.... Your handler reads the timestamp, computes abs(now - t), and rejects if the delta exceeds a tolerance window (Stripe recommends 5 minutes). This blocks replays older than the window. For replays *within* the window — an attacker who's fast — the second defense is idempotency: your handler stores every event ID it has processed and returns 200 immediately on duplicates without re-running business logic.

Provider behavior varies. Stripe and Slack include timestamps in the signed material. GitHub does not — X-Hub-Signature-256 covers only the body. Shopify includes a timestamp via X-Shopify-Webhook-Timestamp but signs only the body, so verifying timestamp authenticity requires a separate check (or trust in the TLS layer, which is acceptable for HTTPS-only webhooks).

What about replays from beyond your tolerance window? They fail timestamp check, your handler returns 401, you log "stale webhook." This is normal; don't alert on isolated stale-webhook errors — they happen when retries chain across long delays or when an old delivery surfaces from a queue that backed up.

Don't try to defend against replay with "I've seen this body before" — bodies of legitimate retries are byte-identical to bodies of the first attempt. The event ID + timestamp combination is the right signal; raw-body uniqueness is not.

See Replay Attack in real traffic

WebhookWhisper captures every webhook with full headers, body, signature, and timing — so concepts like replay attack stop being abstract and become something you can inspect.

Start Free