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:

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:

# 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.

Test: Cross-origin credential replay Register a credential at 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.

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:

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:

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:

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:

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:

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:

Critical test: fallback security Test the fallback path separately. If users can still authenticate via email magic link, SMS OTP, or password when they "can't find their passkey," the application's security is bounded by the weakest fallback — not by WebAuthn.

WebAuthn Security Testing Checklist

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
← Back to Blog