October 30, 2026
A Webhook With No Memory: Replay and Forgery in a Payment Notification Endpoint
Webhooks occupy a strange trust position in modern application architecture. They’re incoming requests, arriving over the open internet…

By T4nv1
4 min read
Webhooks occupy a strange trust position in modern application architecture. They're incoming requests, arriving over the open internet exactly like any request from a browser or an attacker, yet they're routinely treated with a level of implicit trust closer to an internal system call, because the payload describes something that sounds authoritative — a payment succeeded, a subscription renewed, an order shipped. This bug existed because that trust was never actually earned through proper verification, and because the endpoint had no memory of what it had already processed.
The target, which I'll call Ferrier Commerce here per their program's terms, is an online marketplace receiving payment-status webhooks from a third-party payment processor, used to mark orders as paid and release digital goods to the purchasing customer automatically.
The Two Separate Gaps, Found One at a Time
I want to walk through this as two genuinely separate findings, because they compound into something considerably worse together than either represents alone, and a report conflating them risks understating the real severity.
Gap One: No Signature Verification
Payment processors almost universally sign their webhook payloads with a secret shared between the processor and the receiving merchant, specifically so the receiving endpoint can verify a given notification genuinely originated from the processor rather than from anyone who simply knows the endpoint's URL and the expected payload shape. I captured a legitimate webhook payload from a real test transaction, including its signature header, then modified the payload's order amount and order ID while leaving the original, now-mismatched signature attached, and submitted it directly to Ferrier's webhook endpoint:
POST /webhooks/payment-status
X-Signature: <original, now-invalid signature>
{
"orderId": "ORD-88213",
"status": "paid",
"amount": 499.00
}POST /webhooks/payment-status
X-Signature: <original, now-invalid signature>
{
"orderId": "ORD-88213",
"status": "paid",
"amount": 499.00
}This was accepted and processed as a legitimate payment confirmation, marking the referenced order as paid and triggering digital goods release, despite the attached signature no longer corresponding to the modified payload at all. The endpoint, it turned out, wasn't validating the signature against the payload — it was present in the request, checked for mere existence as a header perhaps, but never cryptographically verified against the actual body content.
Gap Two: No Replay Protection
A separate, equally serious gap surfaced when I resubmitted an entirely unmodified, genuinely legitimate webhook payload — one that had already been processed once, for a real, completed test order — a second time, unchanged, several minutes later:
POST /webhooks/payment-status
X-Signature: <original, genuinely valid signature for this exact unmodified payload>
{
"orderId": "ORD-88190",
"status": "paid",
"amount": 29.99
}POST /webhooks/payment-status
X-Signature: <original, genuinely valid signature for this exact unmodified payload>
{
"orderId": "ORD-88190",
"status": "paid",
"amount": 29.99
}The endpoint processed it again, a second time, as if it were a new, independent payment event — applying whatever order-fulfillment logic that status update triggered once more, with no check for whether this exact payload, or even this exact order ID combined with this status, had already been received and processed previously.
Combining Both Into a Complete Attack
Individually, each gap is a real problem. Together, they eliminate the need for any payload modification at all: an attacker only needs to observe or intercept a single genuine, legitimate "payment succeeded" webhook once — which, depending on the platform's broader architecture, might be achievable through means entirely unrelated to breaking any cryptography, such as a logging misconfiguration or a referrer leak exposing the payload incidentally — and can then replay that exact, genuinely-signed payload indefinitely, triggering the same fulfillment action repeatedly, as many times as desired, for the same original order, with every replay carrying a perfectly valid signature since nothing about the payload itself was ever modified.
I confirmed this specifically using a webhook generated from my own genuine, real test purchase, replaying the identical captured payload multiple times and confirming each replay triggered fulfillment logic again, each time, with no deduplication or idempotency check preventing it.
Why This Matters Beyond a Single Double-Fulfilled Order
The combination of no signature validation and no replay protection means the webhook endpoint effectively trusts any syntactically valid payload shape submitted to it, since the first gap means an attacker doesn't even need a genuine signature to begin with — they can simply construct an entirely fabricated payload for an order ID of their choosing, with no real underlying transaction behind it at all, and have it processed as if a genuine payment had occurred.
Scope
All testing used orders and payloads generated through my own genuine test purchases, and I limited forged-payload testing to modifying amounts and referencing order IDs I owned, never attempting fabrication against any order ID belonging to another customer.
The Report
Title: Payment webhook endpoint lacks signature verification and replay protection, allowing forged and replayed payment confirmations
Root cause: The webhook endpoint accepts a signature header without cryptographically validating it against the actual payload content, allowing arbitrary payload modification. Separately, the endpoint applies no deduplication or idempotency check against previously processed payloads, allowing a genuinely signed, legitimate payload to be replayed indefinitely, each replay triggering fulfillment logic anew.
Reproduction: A modified payload with mismatched signature accepted and processed as valid; a genuine, unmodified payload successfully replayed multiple times, each replay independently triggering order fulfillment — all performed using self-generated test orders and payloads.
Impact: An attacker able to construct or observe a syntactically valid webhook payload can trigger fraudulent order fulfillment without any genuine underlying payment, and can repeatedly replay any single genuine payment confirmation to trigger fulfillment multiple times for a single real transaction.
Fix recommendations:
- Implement proper signature verification, computing the expected signature from the actual received payload and the shared secret, rejecting any request where the computed and provided signatures don't match exactly
- Add idempotency enforcement keyed to a unique identifier the payment processor includes with each genuine event, rejecting or no-op'ing any webhook whose identifier has already been processed
- Log and alert on signature validation failures specifically, since a pattern of failed signatures is a strong, early indicator of active forgery attempts worth investigating promptly
What Happened After
The platform implemented proper cryptographic signature verification and event-level idempotency tracking within the same patch cycle, treating both gaps as components of a single root issue — a webhook endpoint that had never been built with an adversarial sender in mind.
Why Webhook Endpoints Deserve the Same Scrutiny as Any Public API
A webhook URL is a public endpoint the moment it's reachable over the internet, regardless of how the parties on either end think about it conceptually, and it deserves exactly the same adversarial testing as any other public API surface: verify that signatures are actually checked against content rather than merely present, and verify that replaying a genuine, captured payload doesn't silently repeat whatever action it originally triggered. Both checks take minutes to run and are consistently worth running on any integration involving incoming, event-triggering webhooks.