WebAuthn Challenge Replay Bypass: When Nonces Are Not Bound
WebAuthn feels unphishable until someone stores challenges poorly. The browser ceremony is strong. The server checklist is where bounty bugs land.
You are hunting replay: a captured authenticatorData + signature accepted twice, or accepted from the wrong ceremony.
Map the ceremony
Registration and authentication each return a server challenge. In DevTools or Burp, find:
GET/POSTthat issuespublicKey.challenge- The assertion/attestation POST that returns session cookies
Decode the challenge (base64url). Confirm whether it is random per attempt and whether it expires. Reuse the same challenge bytes in a second login after a successful one. If the server still accepts the old assertion, you have a replay bug.
Some apps cache challenges in Redis without a one-time flag. Others bind the challenge to a user id but not to origin / rpId. Test one variable at a time. Parallel tabs that start two logins can expose whether challenges are global or session-scoped—race both finish paths if the program allows careful concurrency tests.
Practical replay steps
- Complete a legitimate login, save the assertion JSON.
- Log out (or use a private window) and resubmit the saved assertion.
- Try submitting it under a different account's login start if the challenge is global.
- Retry after the advertised TTL; expired-but-accepted is still a finding.
- Flip
clientDataJSON.originonly if you are testing server parsing—and know that authenticators sign over client data, so naive edits fail verification. Real bugs are usually server skipping verify, or accepting an assertion whose challenge was never issued for that session.
I've seen servers verify the signature against the credential public key but forget to check that the challenge matches the outstanding login. Signature valid, ceremony wrong. Still a bypass.
Token binding gaps show up in APIs that accept assertions from mobile SDKs with a looser JSON schema than the web path. Compare field checks across clients. If the mobile path omits origin verification while web enforces it, document both request shapes in one report—same root cause family, clearer impact.
Related weak spots
- Challenges that live forever in a JWT you can rewind
- Parallel login starts that issue duplicate challenges for the same user
- Native apps that punt assertion blobs to a webview endpoint with looser checks
- Registration ceremonies that never invalidate a pending challenge after success, leaving a second attestation accept
Report shape
Include timestamps, challenge values, and proof the second acceptance minted a session. Recommend single-use challenge store, short TTL, hard bind to user + origin + rpId, and rejecting assertions with mismatched clientDataJSON.challenge.
Call out whether the replay minted a full session cookie, a step-up token, or only a partial login state. Triage maps severity to what the second acceptance actually unlocked. If you needed the victim's credential id from a prior leak, say that dependency clearly so the report is not oversold.
Passkeys fail closed when the server is strict. Replay shows where it was not.