⚠️ No CSP — your next Register click will be hijacked *during* the biometric prompt
← All demos · Passkeys Demo 2 of 4 · CSP

Gesture-Preserving Forgery

A malicious script hooks navigator.credentials.create. You see your real biometric prompt and complete a real ceremony — and the script silently substitutes its own keypair before the credential reaches the server.

Hook armed — your next Register click will trigger a real ceremony, then the result will be silently swapped

Set up a passkey

Logged in as . Click Register passkey and complete the biometric / security key prompt your browser shows you. Watch the activity log and credentials list as it happens.

🚨 Hijack hook armed. A script from evil-cyber-hacker.com has monkey-patched navigator.credentials.create. When you click Register, your authenticator's real prompt will fire and you will complete a genuine biometric. The hook then drops the real attestation and substitutes a synthetic credential built with an attacker-controlled keypair. The page submits the substituted credential as if nothing had happened — the server has no way to tell the difference.

💡 Open DevTools → Network. After you click Register, watch the POST to /passkeys/api/register/finish.php carry an attestationObject whose embedded public key you don't possess, and the beacon to evil-cyber-hacker.com/demo/steal/ exfiltrating the JWK.

↪️ Sister demo: /passkeys/1/ shows the same outcome without any user interaction — useful counter-example to "but the user would notice if no prompt fired".

What's happening

The hook installs at script-load time and waits. When the page calls navigator.credentials.create(), the hook forwards to the real implementation so your authenticator does its normal thing — you see Touch ID / Windows Hello / your security key, you confirm, the device generates a real keypair and signs a real attestation. The hook then throws that response away and constructs its own: a fake attestationObject with fmt:"none", an attacker-generated public key embedded in authData, and the *real* clientDataJSON from the original ceremony (so origin and challenge bytes still match what the server expects). The page submits this synthetic credential. The server stores it, indistinguishable from a legitimate registration.

Pedagogical takeaway: the biometric prompt validates *you*, not the credential the page sends to the server. Anything between navigator.credentials.create() and your /register/finish handler can replace the keypair without touching the prompt.

Credentials registered to your account

Live — polls /passkeys/api/credentials.php every second. Forgeries from this attack are submitted with the page's normal label (legitimate), since the server can't tell them apart from real ones — the page detects the forgery via the in-memory attacker stash and flags it for you.

  • Loading…

Activity log

Real-time trace of legitimate ceremony steps, hook installation and substitution events, and CSP violations.

    ← Demo 1 Demo 3 →