Replaying a Signed Withdrawal: Nonce Reuse and Double-Spend
The challenge
This custodial wallet API signs each withdrawal with a one-time nonce so a request cannot be used twice. Except it never records which nonces it has already spent. Send your fresh withdrawal first to see it succeed, then resend a nonce from an earlier signed withdrawal and watch the same money leave twice. Read the flag the double-spend leaks.
What you'll learn
- Recognize a replay attack where a previously valid signed request is resent to repeat its effect
- Understand the purpose of a one-time nonce and how it fails when not tracked server-side
- Run a two-step exploit: send a fresh request, then replay an earlier one
- Read API responses to confirm a double-spend was accepted
- Explain why nonces must be durably marked consumed and duplicates rejected atomically
Skills tested
Prerequisites
- Basic understanding of HTTP requests and JSON bodies
- Familiarity with the idea of a nonce as a one-time value
- Awareness that a withdrawal moves funds and should happen at most once per authorization
How it works
A nonce (number used once) is meant to make a signed request single-use: each withdrawal is authorized with a unique nonce so that capturing and resending the request cannot move the money again. The protection only works if the server remembers which nonces it has already settled and refuses any repeat. This API generates and checks the signature on the nonce but never persists a spent-nonce record, so the same nonce can be presented as many times as the attacker likes.
That gap turns into a replay attack. An earlier withdrawal - nonce 41 - was already signed and completed. Because nothing marks 41 as consumed, resending it is accepted again and the wallet broadcasts the identical transfer a second time, a double-spend. No signature was forged and no value was edited; the original, legitimately signed request was simply replayed. This is one of the most common flaws in payment, wallet, and authorization flows, and it is why idempotency is a security property, not just a reliability one.
The exploit is inherently two-step, which is what places it above a single-field flip: you first send your own fresh nonce to establish that the endpoint works and to see what a real settlement looks like, then you replay a prior nonce and observe the duplicate being accepted. Understanding why 42 succeeds once but 41 succeeds again is the whole lesson.
Common mistakes
- Incrementing the nonce to a future value like
43and giving up on the409- higher nonces are not yet signed by the wallet, so they are correctly rejected. - Assuming the signature on the request makes it safe to resend, when a signature proves authenticity, not single-use.
- Sending only the fresh nonce and concluding the API is fine, missing that the flaw appears only when an already-settled nonce is replayed.
- Treating this as a tampering bug rather than the real flaw: the request is unchanged and legitimately signed; the server simply forgot it had already honored that nonce.
How to defend against it
Make each nonce truly one-time by durably recording consumption and rejecting any reuse before funds move. A nonce that is not tracked is decoration, not defense.
- Persist every settled nonce per account and reject duplicates; or enforce a strictly increasing per-account sequence and refuse anything not greater than the last settled value.
- Make the spend check and the settlement atomic (a single transaction or a unique constraint on account plus nonce) so concurrent replays cannot both win the race.
- Bind the signed authorization to all material fields (amount, asset, destination, nonce, chain id) and a short expiry, so a captured request cannot be reused later or against a different transfer.
- Log and alert on any request presenting an already-consumed nonce, since that is an unambiguous replay attempt.