WebAuthn and FIDO2 Passkey Security Testing
WebAuthn and FIDO2 represent the most significant shift in authentication security in a decade. Passkeys eliminate passwords, replace SMS 2FA, and — when implemented correctly — defeat phishing entirely. But "when implemented correctly" is doing a lot of work in that sentence. The server-side verification logic is non-trivial, libraries vary in quality, and implementation mistakes can undermine the security guarantees completely.
This guide covers how to security test WebAuthn and FIDO2 passkey implementations — from registration through authentication — including the server-side checks that must be verified and the attack vectors that arise when they're skipped.
WebAuthn Architecture: What You're Testing
Understanding the protocol is prerequisite to testing it. A WebAuthn flow has three parties:
- Relying Party (RP): your server/backend — the entity being authenticated to
- Authenticator: the device generating the credential (passkey in platform authenticator, hardware key for roaming authenticator)
- Client (browser): mediates between RP and authenticator, enforces origin binding
There are two ceremonies: registration (creating a credential) and authentication (proving you hold the credential). Both involve a challenge/response flow with signed CBOR data. Your testing must cover both ceremonies independently — vulnerabilities often appear in one but not the other.
Registration Ceremony Vulnerabilities
Challenge Not Validated (or Reused)
The server must generate a random challenge for each registration and verify the attestation response contains that exact challenge. Failures:
- Fixed challenge: the server always uses the same challenge. Test by resubmitting old registration data — it should be rejected but won't be.
- Challenge not verified: the server ignores the challenge field in the attestation response entirely.
- Predictable challenge: sequential or timestamp-based challenges allow an attacker to pre-generate valid attestation responses.
# Intercept registration in Burp Suite
# 1. Start registration flow — capture the /register/begin response
# Note the challenge value from the server's PublicKeyCredentialCreationOptions
# 2. Complete registration — capture the /register/complete request
# Contains authenticatorAttestationResponse with clientDataJSON
# 3. Decode clientDataJSON (base64 → JSON)
echo "BASE64_CLIENT_DATA_JSON" | base64 -d | python3 -m json.tool
# Should contain: {"type":"webauthn.create","challenge":"...","origin":"..."}
# 4. Replay old registration data with a new session
# If the server accepts it, challenge validation is broken
Origin Not Verified
The clientDataJSON includes the origin field, which the browser sets to the page's origin. The RP must verify this matches its own origin. If it doesn't, an attacker who intercepts a registration response from a phishing domain can replay it against the legitimate RP.
https://evil.example.com (which mirrors the target's WebAuthn API). Attempt to submit the attestation response to the target RP at https://target.com. The RP must reject it — if the origin check is skipped, the attacker's credential is accepted.
# Decode the clientDataJSON from a registration at evil.example.com
# It will contain "origin": "https://evil.example.com"
# Submit this to https://target.com/register/complete
# Correct behavior: 400/rejection due to origin mismatch
# Vulnerable behavior: 200/success
RPID Not Verified
The authenticator signs the rpId into the authenticatorData as a hash of the relying party ID (SHA-256). The server must verify this hash matches its own rpId. Without this check, credentials registered for a subdomain could be used to authenticate to the parent domain, or vice versa.
- Register a credential with
rpId: "sub.target.com" - Attempt authentication against
target.com - The server should reject the
rpIdHashmismatch
Attestation Verification Skipped
Attestation allows the RP to verify the authenticator's provenance (make, model, and that it's a genuine FIDO device). Most applications set attestation to none or skip verification. This is often acceptable but worth documenting, especially for high-assurance applications that claim FIDO certification compliance.
Test: submit a registration response with forged attestation data (or none statement where direct was expected). If the server accepts it without error, attestation verification is not implemented.
Authentication Ceremony Vulnerabilities
Sign Counter Not Checked (Cloned Authenticator Risk)
Authenticators include a signCount in each authentication response that increments with every use. The RP must store the last seen sign count and reject any authentication where the count is not greater than the stored value. Skipping this check means a cloned authenticator (a copy of a passkey extracted from a compromised device) can authenticate indefinitely without detection.
# Test: authentication replay with stale sign count
# 1. Authenticate and note the signCount in the authenticatorData
# 2. Replay the exact same authentication response
# Correct: server rejects (count must be greater than stored)
# Vulnerable: server accepts (no counter check)
# Decode authenticatorData to inspect signCount
# authenticatorData structure: rpIdHash(32) + flags(1) + signCount(4) + ...
python3 -c "
import base64, struct
auth_data = base64.b64decode('YOUR_AUTH_DATA_BASE64')
sign_count = struct.unpack('>I', auth_data[33:37])[0]
print(f'Sign count: {sign_count}')
"
Challenge Not Single-Use
Authentication challenges must be single-use and time-limited. Test:
- Complete authentication and capture the signed assertion
- Replay the same assertion a second time (same session, new session)
- Correct behavior: rejected (challenge already consumed)
- Vulnerable behavior: accepted (challenge can be replayed)
User Presence vs User Verification Not Enforced
WebAuthn defines two security levels:
| Flag | Meaning | Bit in flags byte |
|---|---|---|
| User Presence (UP) | User physically interacted with authenticator (button press) | Bit 0 |
| User Verification (UV) | User verified their identity (PIN, biometric) | Bit 2 |
If the application requires UV (e.g., for admin actions or step-up authentication), the server must check the UV bit. Test:
# Authenticate with a UV-capable authenticator but bypass biometric/PIN
# (some softtoken implementations allow this)
# Submit assertion with UV bit = 0 in flags
# Correct: server rejects due to missing UV flag
# Vulnerable: server accepts without checking UV bit
# Check flags byte from authenticatorData
python3 -c "
import base64
auth_data = base64.b64decode('YOUR_AUTH_DATA_BASE64')
flags = auth_data[32]
up = bool(flags & 0x01) # User Presence
uv = bool(flags & 0x04) # User Verification
print(f'UP: {up}, UV: {uv}')
"
Credential ID Not Bound to User
The RP must verify that the credential ID in the authentication request belongs to the user being authenticated. Without this check, an attacker can authenticate as any user by submitting their own credential ID for a different user's account.
# Test: credential substitution
# 1. Register credential for attacker account — get credential ID
# 2. Initiate authentication for victim account
# 3. Modify the assertion: replace victim's allowedCredentials with attacker's credential ID
# 4. Complete authentication with attacker's authenticator
# Correct: server rejects (credential not registered to this user)
# Vulnerable: server accepts (credential not bound to user ID)
Implementation-Level Testing
Library Version Vulnerabilities
WebAuthn server libraries have had implementation bugs. Identify which library the application uses:
- Node.js:
@simplewebauthn/server,fido2-lib - Python:
py_webauthn,webauthn - Java:
webauthn4j,java-webauthn-server - PHP:
web-auth/webauthn-framework - Go:
go-webauthn/webauthn
Check CVE databases and GitHub releases for known vulnerabilities in the version in use. Common library bugs include failing to verify the RP ID hash, not validating the CBOR encoding, or accepting null signatures.
CBOR Parsing Edge Cases
WebAuthn uses CBOR (Concise Binary Object Representation) encoding for authenticator data. Some parsers have type confusion vulnerabilities where integer map keys can shadow string keys. Test by submitting malformed CBOR:
# Send authentication response with malformed CBOR in authenticatorData
# Look for unhandled exceptions, 500 errors, or unexpected success responses
# Tools: cbor2 (Python), cbor-redux (JavaScript)
Concurrent Registration Race Condition
If a user can initiate multiple registration ceremonies simultaneously, a race condition may allow registering multiple credentials for the same challenge, or registering a credential tied to another user's challenge. Test with parallel requests using tools like Turbo Intruder:
- Send two concurrent
/register/beginrequests - Complete both ceremonies and submit both to
/register/complete - Check whether both credentials are stored and which challenge each is bound to
Passkey-Specific Attack Surfaces
Cross-Device Passkey Sync Risks
Platform passkeys (Apple Passkeys, Google Password Manager passkeys) sync across devices via cloud. The security posture of the cloud account now affects the passkey's security. While this is outside the RP's control, test:
- Whether the RP exposes credential metadata that reveals which authenticator type was used
- Whether the RP has device management controls (list credentials, revoke specific authenticators)
- Whether lost device flows correctly invalidate synced passkeys
Phishing-Resistant Claims
WebAuthn's phishing resistance depends on the browser enforcing origin binding. If the RP's authentication page can be loaded in a frame or via an open redirect, verify:
- The application sets
X-Frame-Options: DENYorContent-Security-Policy: frame-ancestors 'none' - No open redirects exist that could forward users to a phishing domain while retaining a legitimate-looking URL
- The passkey prompt appears only on the legitimate origin (not via iframe proxying from a phishing page)
Fallback Authentication Weaknesses
The most common WebAuthn security failure is a weak fallback. Most applications retain password or SMS OTP fallback when passkeys fail or are unavailable. This entirely undermines phishing resistance:
WebAuthn Security Testing Checklist
- Verify challenge is random, single-use, and expires after a reasonable timeout (60-300s)
- Verify the RP validates the
originfield inclientDataJSON - Verify the RP validates the
rpIdHashinauthenticatorData - Test credential ID binding — verify credentials cannot be used across user accounts
- Test sign counter enforcement — replay old authentication data and confirm rejection
- Test authentication replay — submit a valid assertion a second time
- Verify UV bit is checked when user verification is required
- Test concurrent registration for race conditions
- Identify WebAuthn library and check for known CVEs in that version
- Evaluate the strength of the fallback authentication mechanism
- Check credential management: can users list, rename, and revoke registered passkeys?
- Verify that removed/revoked credentials are rejected at authentication time
- Check for Clickjacking protection on the registration and authentication pages
Ironimo scans web applications for authentication vulnerabilities including misconfigured WebAuthn implementations, weak fallback flows, and session management issues. Kali Linux tooling, no configuration required.
Start free scan