Chapter 1 · The scenario
Example: A voucher bot replayed in Indore
An electronics retailer in Indore runs a WhatsApp bot that issues a store-credit voucher when a customer taps "Yes, refund to wallet" on an interactive message. During integration testing, an engineer pasted real webhook deliveries, headers included, into a public request-inspection site. Weeks later someone resent one of those deliveries 40 times. Every copy passed the X-Hub-Signature-256 check, and because the handler deduplicated nothing, it issued 40 vouchers to one account before finance noticed. The fix took a day: an atomic insert of each inbound wamid into a dedup store with an eight-day expiry before any action, a rule that drops events older than eight days, a rotated app secret so the leaked copies no longer verify, and a separate test app whose secret never touches production.
Chapter 2 · What is a replay attack?
A replay attack is an attack in which someone captures a legitimate message (a signed webhook delivery, an authenticated API request, a step in a login exchange) and sends it again later, unchanged. Because nothing has been altered, every integrity check passes: the signature is genuine, the token is valid and the JSON is well formed. What makes it an attack is repetition at the wrong time: a refund credited twice, an order confirmed again, a one-time action triggered on demand by someone who was never meant to trigger it.
This is the gap a Hash-Based Message Authentication Code (HMAC) leaves open. Meta signs every WhatsApp webhook in the X-Hub-Signature-256 header with an HMAC-SHA256 of the raw body keyed with your App Secret, which proves the body came from Meta and was not modified. The signature covers the body only, though: there is no signed delivery timestamp and no nonce, so a copy replayed next week verifies exactly as it did the first time. Copies are rarely sniffed off the wire, because TLS protects transit; they leak from verbose request logs, debugging proxies, public request-inspection tools used during Webhook Testing and fixture files committed with real headers.
Defence therefore happens after the signature check. Idempotent processing with Webhook Deduplication, keyed on the event's identity such as the WhatsApp Message ID (wamid) plus its status, turns a replayed event into a harmless no-op. Freshness checks on the event timestamp reject anything older than a genuine retry could be, and in request formats you control, a signed timestamp plus a single-use nonce shrinks the usable window to minutes. A replay attack is a hostile resend; do not confuse it with webhook replay, the deliberate reprocessing of your own stored events after a bug fix, which relies on the same idempotency for the opposite purpose.
Key takeaways
- A replay attack resends a captured, genuine request, so the receiver repeats an action the real sender asked for only once.
- A correct HMAC does not stop a replay: the copied body and signature are untouched, so
X-Hub-Signature-256still verifies. - Meta's webhook signature covers only the raw body, with no signed timestamp or nonce, so deduplication by event identity is your main defence.
- Meta retries webhooks for up to 7 days, so a tight freshness window on event timestamps would drop genuine retries; keep dedup keys longer than that.
- In signature schemes you design yourself, sign a timestamp and a nonce with the body and reject anything stale or already seen.
Chapter 3 · How it unfolds
How does a replay attack work?
A genuine request is captured
The attacker obtains a real delivery, the raw body plus its
X-Hub-Signature-256header, typically from full request logs, a misconfigured proxy, a public request bin used for testing or a leaked fixture file.It is resent unchanged
Hours or days later they POST the identical bytes and header to your endpoint. Nothing has to be forged, because nothing is modified, and the app secret is never needed.
The signature still verifies
Your server recomputes HMAC-SHA256 over the raw body with the app secret and gets the same digest as the original. A signature check alone cannot tell a copy from the first delivery.
Deduplication stops the repeat
Before any side effect, record the event's identity atomically: the
wamidfor an inbound message, thewamidplusstatusfor a status update. If the key already exists, return 200 and do nothing else.Freshness and ordering close the gaps
Reject events whose
timestamplies in the future or beyond Meta's 7-day retry window plus a margin, never move a message's status backwards, and expire dedup keys only after that window has passed.
Chapter 4 · In code
A replay attack in code
import crypto from "node:crypto";import { redis } from "./redis.js"; // ioredis clientconst MAX_AGE_S = 8 * 24 * 3600; // Meta retries for up to 7 days; add a marginfunction validSignature(rawBody, header = "") {const expected = "sha256=" + crypto.createHmac("sha256", process.env.APP_SECRET).update(rawBody).digest("hex");return header.length === expected.length &&crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));}export async function handleWebhook(req, res) {// 1. Authenticity: proves Meta signed these bytes, NOT that they are new.if (!validSignature(req.rawBody, req.get("X-Hub-Signature-256"))) return res.sendStatus(401);const now = Math.floor(Date.now() / 1000);for (const entry of req.body.entry ?? []) {for (const { value = {} } of entry.changes ?? []) {const events = [...(value.messages ?? []).map((m) => ({ key: `msg:${m.id}`, ts: Number(m.timestamp), data: m })),...(value.statuses ?? []).map((s) => ({ key: `st:${s.id}:${s.status}`, ts: Number(s.timestamp), data: s })),];for (const ev of events) {// 2. Freshness: nothing from the future, nothing older than any genuine retry.if (ev.ts > now + 300 || now - ev.ts > MAX_AGE_S) continue;// 3. Deduplication: atomic "first writer wins", so a replay becomes a no-op.const first = await redis.set(`wa:${ev.key}`, "1", "EX", MAX_AGE_S, "NX");if (first === "OK") await queue.add("whatsapp-event", ev.data);}}}return res.sendStatus(200); // answer replays the same way: they learn nothing}
The signature check runs first, but it is the atomic SET NX on the event key that actually stops a replay.
Chapter 5 · Where it goes wrong
Common mistakes with a replay attack
Treating a valid signature as proof of freshness
An HMAC proves who signed the bytes and that they are unchanged, nothing more. Put deduplication and freshness checks after the signature check and before any side effect.
A five-minute window on WhatsApp event timestamps
The event
timestamprecords when the event happened, not when it was delivered, and Meta retries for up to seven days. A tight window silently drops genuine retries after an outage; use a window longer than the retry period and rely on deduplication.Check-then-insert deduplication
Reading "have we seen this?" and writing the key later lets two concurrent copies both pass. Use one atomic operation, such as SET NX or an insert on a unique key, and act only if your write won.
Leaking replayable requests yourself
Full request logs, public request bins and fixtures with real signatures hand out ready-made replays. Mask logs, test with a separate app and secret, and rotate the app secret if real deliveries were exposed, which makes the leaked copies fail verification.
Expiring dedup keys too early
A key kept for an hour lets tomorrow's replay through. Keep keys at least as long as the oldest event your freshness check still accepts.
Chapter 6 · Why it matters
Why does a replay attack matter?
Most webhook handlers cause side effects: issuing a refund or voucher, confirming a booking, sending a follow-up template, moving a CRM deal, crediting a wallet. If a replayed delivery is treated as new, anyone holding one old payload, or an engineer pasting a fixture into production, can repeat those effects at will, and every extra outbound template is another charged message. A signature check answers only "did Meta send these bytes?"; replay protection answers "have we already acted on them?", and that is the question that protects money, stock and customers' trust.
Chapter 7 · The facts
A replay attack: key facts and limits
| Meta webhook signature | X-Hub-Signature-256: HMAC-SHA256 of the raw body |
|---|---|
| Signed timestamp or nonce (Meta) | None: the signature covers the body only |
| Meta webhook retries | Decreasing frequency, up to 7 days |
| Dedup key | wamid for messages; wamid + status for statuses |
| Dedup key lifetime | Longer than the 7-day retry window, e.g. 8 days |
| Event timestamp format | Unix seconds, sent as a string |
| Typical tolerance in schemes you design | About 5 minutes |
| Nonce | At least 128 random bits, accepted once |
Chapter 8 · Replay Attack FAQs
Replay Attack FAQs
Does HMAC signature verification prevent replay attacks?+
How do I stop replay attacks on WhatsApp webhooks?+
What is the difference between a replay attack and webhook replay?+
Should I use a nonce or a timestamp to prevent replays?+
Does TLS stop replay attacks?+
Chapter 9 · What good teams do
Best practices for a replay attack
- Return 200 for a detected duplicate without processing it, so a replayer cannot tell what happened.
- Alert on bursts of duplicate keys from one source: it is either an attack or a broken retry loop.
- Make side effects idempotent as a second line of defence, for example with an idempotency key on refunds.
- Single-use OTPs and login challenges are replay defences too; mark them used the moment they verify.