Why phishing proxies fail against passkeys, and where they still win
The dominant phishing technique today is not a fake login page but a transparent proxy that sits between the victim and the real site. It defeats one-time codes completely. Against an origin-bound assertion it fails at a specific, unavoidable point.
Why do phishing proxies fail against passkeys?
Because the browser asserts the origin the ceremony was invoked from, and page JavaScript cannot alter it. The proxy relays everything else perfectly — passwords, one-time codes, push approvals — but the authenticator will not sign for a domain the credential was not registered to.
- The proxy fails because the authenticator signs the origin it was actually invoked from, and the browser will not lie about it.
- That protection covers the authentication ceremony. It does not cover a session token issued afterwards, which remains stealable.
- The remaining attack surface is downgrade paths and post-authentication actions, which is where deployments actually lose.
Part of Privileged identity and account recovery
How the proxy works against everything else
The victim reaches a lookalike domain. The kit forwards every request to the genuine site and every response back, rewriting links as it goes. The victim sees the real site because they are seeing the real site.
Passwords pass through and are captured. One-time codes pass through and are captured. Push approvals are triggered by the proxy's own login and approved by the confused victim. At the end, the kit holds the session cookie the real site issued.
Nothing about the victim's behaviour prevents this. The page is genuine, the certificate is valid, and the only signal is the domain name.
The exact point of failure
WebAuthn's client data includes the origin the ceremony was invoked from. The browser fills this in; page JavaScript cannot alter it.
# The victim is on the proxy's domain.
clientDataJSON = {
"type": "webauthn.get",
"challenge": "<relayed from the real site>",
"origin": "https://login.exarnple-corp.com" ← the proxy
}
# The authenticator signs over sha256(clientDataJSON)
# together with rpIdHash, which is derived from the RP ID
# the browser scoped the credential to.
# The real server verifies:
require(clientData.origin in ALLOWED_ORIGINS) ← FAILS
require(authData.rpIdHash == sha256("example-corp.com"))
Two independent checks fail. The origin in client data is the proxy's, and the credential is scoped to the genuine RP ID so the browser will not surface it for a different domain in the first place.
The proxy cannot rewrite the origin because it is not in the HTML — it is asserted by the browser about the page it is running. Rewriting it would require compromising the browser, which is a different class of attack.
Where the proxy still wins
This is the part that gets dropped from the summary, and it is where deployments actually fail.
| Attack path | Does the passkey stop it? |
|---|---|
| Relay the authentication ceremony | Yes — origin binding fails |
| Offer a fallback to OTP or password on the proxy page | No — the victim never invokes the authenticator |
| Steal the session cookie after a genuine login elsewhere | No — nothing binds the session to the ceremony |
| Trigger account recovery through the help desk | No — a different path entirely |
| Act inside a stolen session | No — no further assertion is required |
Rows two and three are how real compromises happen at organisations that have deployed passkeys. The phishing-resistant mechanism was present and was routed around.
Closing the downgrade path
A fallback that a user can choose is a fallback an attacker can induce. "Having trouble with your security key? Use a code instead" is a functioning phishing affordance built by the defender.
- Remove weaker authenticators as options once passkey enrolment is complete for a population. Not de-prioritise — remove.
- Make account recovery require a signature from an existing credential rather than a knowledge-based conversation.
- Ensure no self-service path re-enables a weaker factor without the same assurance.
Closing the post-authentication path
Origin binding protects the ceremony. It says nothing about the token issued afterwards, which is an ordinary bearer credential.
The mitigation is to stop treating the session as sufficient for consequential operations. A fresh assertion bound to the specific action means a stolen session yields browsing rather than money movement.
What to check in your own deployment
- Is the origin in client data validated against an explicit allow-list, or merely parsed?
- Is
rpIdHashcompared to the expected value, or assumed correct? - Is the challenge single-use, bound to the session, and time-limited?
- Is the user-verification flag required, or accepted whatever its value?
- Can any user reach a code-based or password-based path from the sign-in screen?
The last question is the one that most often has the wrong answer, and it is the cheapest to fix.
Where the proxy still wins
| Path | Stopped? |
|---|---|
| Relay the ceremony | Yes — origin binding |
| Offer a fallback to a code on the proxy page | No — the authenticator is never invoked |
| Steal the session issued afterwards | No |
| Trigger account recovery via the help desk | No |
| Act inside a stolen session | No |
Rows two and three are how organisations that have deployed passkeys still get compromised. The phishing-resistant mechanism was present and was routed around.
Objections and honest limits
“So passkeys are phishing-proof.” The ceremony is resistant. The session issued afterwards is an ordinary bearer token, and any fallback to a weaker factor is a path the attacker will steer toward.
“Users need a fallback.” Until enrolment is complete, yes. After that, a self-service fallback is a phishing affordance you built — ‘having trouble with your key? use a code instead’.
Checking your own deployment
- Is the origin validated against an allow-list, or merely parsed? Parsing is not checking.
- Is rpIdHash compared, or assumed? Compare it.
- Is the challenge single-use, bound and time-limited? All three.
- Is user verification required, or accepted whatever its value? Require it.
- Can any user reach a code or password path from sign-in? The question with the wrong answer most often.
Terms used here
- Adversary in the middle
- A transparent proxy relaying a live session between victim and genuine site.
- Origin binding
- The browser asserting the domain a ceremony ran on, inside data the authenticator signs.
- Downgrade path
- A weaker authentication option a user can choose, and therefore one an attacker can induce.
Frequently asked questions
Why can't the proxy forge the origin? The browser asserts it about the page it is running, and page JavaScript cannot modify client data. Forging it requires compromising the browser.
Are passkeys phishing-proof then? The authentication ceremony is resistant. The session issued afterwards is an ordinary bearer token, and fallback paths to weaker factors remain fully phishable.
What is the most common deployment mistake? Leaving a self-service fallback to codes or passwords on the sign-in screen. An attacker simply induces the user down that path.
Does this protect actions after login? No. That requires a fresh assertion bound to the specific action, otherwise a stolen session is sufficient.
Where this fits in Manav
Manav requires a fresh assertion bound to the specific action, from a credential under the person's sole control. A stolen session reaches the endpoint and cannot produce it.
Sources and further reading
- W3C Web Authentication: An API for accessing Public Key Credentials Level 3
- CISA — Implementing phishing-resistant MFA
- Published research on adversary-in-the-middle phishing toolkits.
- FIDO Alliance specifications