# Auth code and token replay

Single-use credentials never consumed, wrong expiry field, or signatures not covering the payload

_Area: Web authentication and account takeover. Seen in 4 disclosed reports; one confirmed bounty: $2,000; latest 2026-06-23. Updated 2026-10-23._

## How it works
A credential (authorization code, recovery code, request signature) is meant to be usable once, within a bounded window. The assumption that breaks is that presenting it invalidates it. When the server validates the credential but never marks it consumed, and reads a lifetime from the wrong field, the same value works again. Each replay mints fresh material in the victim's identity. A related break: a signature that does not cover the request body, plus an anti-replay cache that expires before the timestamp window, lets a captured signature be reused with different data.

## Where it shows up
- Exchange handlers that read a code row but never delete or mark it used.
- Expiry checks that read a nested or unrelated field (e.g. requested credential lifetime) instead of the row's own expiry.
- Multi-step flows where decryptable material is returned before the final step consumes the credential.
- Request signatures that omit the payload from the signed data.
- Anti-replay cache TTL equal to or shorter than the timestamp validity window, so clock skew leaves a gap.

## How to review
For every credential, find the exact point it is invalidated and confirm it happens atomically with, or before, any use. The invariant: a credential is valid at most once, and its validity window is read from its own stored expiry. Check the exchange does a conditional consume (delete-or-mark returning the row) so two concurrent requests cannot both succeed. For multi-step flows, confirm no decryptable material is returned until the credential is consumed. For signatures, confirm the signed data covers the body and that the replay cache outlives the timestamp window.

## Variants seen
- OAuth2 code exchange never deletes the code row and checks the wrong expiry column, replaying to mint bridge tokens [E1](https://bugbountylibrary.com/r/h1%3A3734676.md).
- Recovery flow returns wrapped passphrase and keys before the recovery code is consumed at the final step [E2](https://bugbountylibrary.com/r/blog%3Athomashouhou-com-post-logic-vulnerabilities-swisscom-e2ee-cloud-storage.md).
- Request signature excludes the payload and the replay cache (300s) expires inside the timestamp window, allowing reuse with a different body [E3](https://bugbountylibrary.com/r/h1%3A425314.md).

## Why fixes fail
E1 shows a fix-shaped check (an expiry test) that reads the wrong field, so it passes review while leaving the code replayable. Consuming the code only at a later step [E2](https://bugbountylibrary.com/r/blog%3Athomashouhou-com-post-logic-vulnerabilities-swisscom-e2ee-cloud-storage.md) leaves an earlier response sufficient for takeover.

## Impact and severity
Full account takeover in the victim's identity and credential minting [E1](https://bugbountylibrary.com/r/h1%3A3734676.md), persistent access via unwrapped keys [E2](https://bugbountylibrary.com/r/blog%3Athomashouhou-com-post-logic-vulnerabilities-swisscom-e2ee-cloud-storage.md), and full-privilege API key creation from a replayed signature [E3](https://bugbountylibrary.com/r/h1%3A425314.md). Severity rises when the credential is long-lived or the replay window is wide.

## Evidence
- E1: https://bugbountylibrary.com/r/h1%3A3734676.md
- E2: https://bugbountylibrary.com/r/blog%3Athomashouhou-com-post-logic-vulnerabilities-swisscom-e2ee-cloud-storage.md
- E3: https://bugbountylibrary.com/r/h1%3A425314.md
