auditing-payment-state-machine-and-idempotency · git:20260829.b60f9b2 · 2026-08-29 · sha256 b4778d7cd9900780

auditing-payment-state-machine-and-idempotency git:20260829.b60f9b2A

Immutable. This exact content is served forever at /api/v1/blob/b4778d7cd9900780.

---
name: auditing-payment-state-machine-and-idempotency
description: >-
  Audit payment and checkout state machines for transitions an attacker can drive out of order or replay for
  value: an order marked paid before the charge is confirmed, a step that can be skipped or repeated so goods
  ship without settlement, a non-idempotent charge or fulfillment endpoint that double-processes on a retried or
  replayed request, and a refund or cancel that returns value while the underlying charge stays captured. Covers
  checkout, charge, fulfillment, and refund flows where money and goods change hands across a sequence of state
  transitions. Use when a purchase moves through ordered payment states and the transitions and their idempotency
  are the boundary. The out-of-order or replayed transition is the source, the value released without settlement
  is the sink, and the skippable step or non-idempotent handler that allows it is the bug.
license: MIT
---

# Auditing payment state machine and idempotency: value must not move faster than settlement

A checkout is a state machine, and money moves through it in a strict order: an order is placed, a charge is
authorized, the charge settles, and only then are goods fulfilled; refunds and cancels run the sequence
backward. The bugs are transitions that break the order or repeat for value. An order marked paid before the
charge is actually confirmed lets fulfillment run against a payment that never settles. A step that can be
skipped, so fulfillment triggers without a completed charge, or repeated, so one payment yields multiple
fulfillments, releases value the sequence did not authorize. A charge or fulfillment endpoint that is not
idempotent double-processes when a request is retried or deliberately replayed, so a network retry or an
attacker's repeat becomes two charges or two shipments. And a refund or cancel that returns value while the
charge stays captured pays out twice. The audit walks the state machine and tries to move value ahead of, or
more than once per, settlement. You audit this by driving transitions out of order and replaying the ones that
release value.

## When to use

- A purchase moves through ordered payment states (placed, authorized, settled, fulfilled, refunded).
- Fulfillment may trigger before a charge is confirmed, or a paid state may be set ahead of settlement.
- Charge, fulfillment, or refund endpoints may not be idempotent under retry or replay.

## Scope check

Test payment flows only against systems you own or are authorized to assess, on non-production or sandbox
payment paths. Driving transitions and replays moves real value on a live system, so use sandbox payment
credentials and test accounts and never trigger a real charge, refund, or fulfillment that is not yours. If you
can't name the authorization, stop.

## The loop

1. **Establish the intended state machine first.** Map the legal states and the only transitions allowed between
   them, and which transition each value-releasing action (fulfillment, refund, payout) requires as its
   precondition. This is the false-positive killer: a flow where fulfillment requires a confirmed settled charge,
   every transition validates its precondition server-side, and every value-releasing handler is idempotent is
   correct. Name the intended sequence, then test deviations.

2. **Check the paid-state precondition.** Confirm the state that gates fulfillment is set only after the charge
   is confirmed settled by the payment provider, not on client assertion or on authorization alone. An order
   marked paid before settlement, or on a client-supplied status, lets goods ship against a payment that may
   never complete.

3. **Test step skipping and out-of-order transitions.** Attempt to reach a value-releasing state without the
   transitions that should precede it: trigger fulfillment without a completed charge, jump past a verification
   step, or set a later state directly. Confirm each transition validates its precondition on the server and
   refuses an out-of-order jump. A state reachable without its prerequisites is a skip.

4. **Test replay and idempotency of value-releasing handlers.** Replay the charge, fulfillment, and payout
   requests, and retry them with and without any idempotency key. Confirm each is idempotent: a repeated request
   for the same logical operation processes once, not once per delivery. A non-idempotent handler turns a retry
   or a deliberate replay into a double charge or double fulfillment.

5. **Check refund and cancel symmetry.** Confirm a refund or cancel only returns value that was actually
   captured, moves the state consistently, and cannot be repeated to pay out more than once or run while the
   charge remains captured. A refund that credits without reversing the charge, or that replays, is a payout
   primitive.

6. **Confirm and record.** Confirm by releasing value without settlement, a fulfillment on an unconfirmed
   charge, a skipped step reaching a paid state, a replayed handler double-processing, or a repeated refund, all
   on sandbox payment paths and without moving real money. Kill the lead if every value-releasing action requires
   its settled precondition validated server-side and every such handler is idempotent under retry and replay.
   Record the out-of-order or replayed transition, the value-released sink, and the skippable step or
   non-idempotent handler.

## Where payment state trust leaks

- **Paid set before settlement.** Marking an order paid on authorization alone or on client-supplied status
  ships goods against a charge that may never settle.
- **A skippable step reaches fulfillment.** A value-releasing state reachable without its preceding transitions
  lets fulfillment run without a completed charge.
- **A non-idempotent handler double-processes.** A charge, fulfillment, or payout endpoint that acts once per
  request rather than once per operation doubles on retry or replay.
- **A repeatable refund pays out twice.** A refund or cancel that can be replayed, or that credits without
  reversing the capture, returns more value than was taken.
- **Client-driven state transitions.** Trusting a client to assert the next state, rather than deriving it from
  server-confirmed events, lets an attacker set the state that releases value.

## Worked example (a confirm and a kill)

> **Confirm.** A checkout marks the order paid and triggers fulfillment when the browser posts back a success
> status after redirect from the payment page, before the server confirms settlement with the provider. Posting
> the success callback directly, without any real payment, drives the order to paid and ships the goods.
> Separately, replaying the fulfillment request produces a second shipment for one order. **Confirmed** payment
> state bypass via client-asserted paid state and non-idempotent fulfillment, `critical`, remediation = set the
> paid state only from a server-side confirmation of settlement with the provider, require the settled
> precondition before fulfillment, and make fulfillment idempotent per order.
>
> **Kill.** The paid state is set only after the server confirms settlement with the payment provider out of
> band; fulfillment, charge, and payout each validate their settled precondition server-side and refuse an
> out-of-order jump; every value-releasing handler is idempotent per logical operation under retry and replay;
> and refunds return only captured value and cannot repeat. A skipped step or replayed request releases nothing
> extra. **Killed**, `kill_reason` = "value-releasing transitions gated on server-confirmed settlement and
> idempotent under replay; no out-of-order or repeated transition releases value."

## Rationalizations to reject

- *"The payment page redirected back with success."* → A client-delivered success is spoofable; confirm
  settlement server-side with the provider before setting paid.
- *"The charge was authorized."* → Authorization is not settlement; gate fulfillment on the confirmed settled
  state, not on a hold that can be released.
- *"Retries are just network noise."* → A non-idempotent handler turns any retry, benign or malicious, into a
  double charge or double shipment; make it idempotent per operation.
- *"Refunds go through our finance team."* → If the refund endpoint can be replayed or credits without reversing
  the capture, it is a payout primitive regardless of who normally calls it.
- *"The client tracks the order state."* → Client-asserted state lets an attacker set the value-releasing state;
  derive every transition from server-confirmed events.

## Executing this in practice

You need the legal states and transitions, which server-confirmed event gates each value-releasing action, and
whether each such handler is idempotent. On sandbox payment paths, try to mark paid without settlement, skip to
fulfillment, replay the charge and fulfillment and refund handlers, and repeat a refund. Reading the state-
transition code shows the intended order; a value release without settlement or a doubled handler shows whether
it holds.

## Related

- `auditing-payment-callback-and-amount-integrity` - the provider callback that should set the paid state; its
  authenticity and amount checks are the trusted signal this state machine must wait for.
- `hunting-price-and-coupon-manipulation` - manipulating the amount before this state machine runs is the paired
  attack; together they cover paying less and paying nothing.
- `auditing-webhook-authenticity-and-callback-trust` - settlement usually arrives as a webhook; that skill
  verifies the event this flow depends on is genuine.
- `hunting-broken-object-level-authorization` - driving another user's order through these transitions is an
  object-authorization failure on the order; the two meet at the order identifier.
- [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = the out-of-order or replayed transition, sink = the
  value released without settlement, evidence = the skippable step or non-idempotent handler.