Yaamlabs
Case study 26

Payout flow accepted replayed webhooks

Marketplace

The payout flow trusted a webhook. The webhook could be sent twice.

Web & API penetration test, B2B marketplace, severity HIGH, reported 2026

At a glance

  • Challenge. Payout webhook handlers verified the signature but never asked whether the event had already been processed, so a signed callback could be replayed.
  • Approach. We captured a legitimate signed callback in a test environment, resubmitted the same bytes unchanged, and checked the sibling handlers for the same gap.
  • Result. Handlers now record the event identifier, enforce a replay window and guard settled payouts, and we retested the flow and its sibling handlers.
  • Impact. Anyone holding one signed payout callback, from a log, a proxy or a retry, could have executed the same payout again with nothing anomalous to detect.
  • Classification. CWE-294 Authentication Bypass by Capture-replay, OWASP Top 10 (2021): A07 Identification and Authentication Failures. Component: Payout webhook handlers.

The challenge

Signature verification answers one question: did this message come from the expected sender. It does not answer whether this message has already been processed, and those are not the same question.

Without a nonce, a timestamp window, or idempotency on the receiving side, a valid signed message stays valid forever. The signature is a property of the bytes, not of the moment. Anybody who obtains a copy of those bytes, from a log, a proxy, a misdirected retry, an intermediary, or the sender’s own retry mechanism, holds an instruction the receiver will honour.

In a payout flow that means the same instruction can be replayed, and the ledger has no reason to object. Each individual payout is legitimate: correctly signed, correctly formed, correctly attributed. There is no anomaly to detect because nothing anomalous has happened. The system did exactly what it was told, more than once.

It is also a bug that provider documentation warns about and that integrations miss anyway, because the warning lives in the section about idempotency and the handler was written from the section about signatures.

Our approach

We looked at the webhook handlers the way the sender’s own retry logic does: as an endpoint that will be called with the same payload more than once, on purpose, and has to behave correctly when that happens.

We captured a legitimate signed callback in a test environment and resubmitted it. It was accepted. No modification, no forged signature, no timing trick. The same bytes, twice.

Around that single result we checked:

  • Whether the payload carried a nonce or event identifier, and whether the handler recorded it
  • Whether the signature covered a timestamp, and whether that timestamp was enforced
  • Whether state transitions were guarded, so an already-settled payout could not settle again
  • Whether the sibling handlers on the same integration shared the assumption

They did. We reported the pattern rather than the single endpoint, because a replay defence that covers one handler and not its siblings is not a defence. It is a list of the endpoints somebody remembered.

Delivered with the captured payload, the two requests and the resulting state, so it could be reproduced against a test tenant without taking our word for any of it.

From finding to fix

  1. Treated a payout webhook as replayable
  2. Resubmitted a validly signed callback
  3. Payout executed a second time
  4. Added idempotency keys, retested

The outcome

Idempotency keys were added at the handler, so the event identifier is recorded on first processing and a repeat is acknowledged without re-executing, and a replay window was enforced on the signature so a captured payload expires instead of remaining valid indefinitely.

The same two controls were applied to the sibling handlers, and the state transitions were guarded independently, so a payout that has already settled cannot settle again even if both of the other controls are bypassed.

We retested the flow and the sibling handlers after the fix. Reported as HIGH in 2026. Retest included in the engagement.

Check your own system

  • Record the provider event identifier on first processing and acknowledge any repeat without executing it again.
  • Reject webhooks whose signed timestamp falls outside a short replay window, and confirm the timestamp sits inside the signed bytes.
  • Guard payout state transitions in the database so a payout already marked settled cannot settle a second time.
  • Resubmit one captured, unmodified callback to every handler on the integration, not only the one you fixed.

Read more case studies, see Web Application Penetration Testing, or tell us about your system on the contact page.