Manav.id
Developer · 4 min read

Why phishing proxies fail against passkeys, and where they still win

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.

Key takeaways
  • 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.

How the proxy works against everything else

Victim on the proxy's domainlooks genuineChallenge relayed from the real siteproxy works so farclientData.origin = the proxybrowser fills itrpIdHash ≠ expectedcredential not offered
Two independent checks fail, and neither is something the proxy can rewrite.

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 pathDoes the passkey stop it?
Relay the authentication ceremonyYes — origin binding fails
Offer a fallback to OTP or password on the proxy pageNo — the victim never invokes the authenticator
Steal the session cookie after a genuine login elsewhereNo — nothing binds the session to the ceremony
Trigger account recovery through the help deskNo — a different path entirely
Act inside a stolen sessionNo — 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.

  1. Remove weaker authenticators as options once passkey enrolment is complete for a population. Not de-prioritise — remove.
  2. Make account recovery require a signature from an existing credential rather than a knowledge-based conversation.
  3. 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

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

The rows that get dropped from the summary
PathStopped?
Relay the ceremonyYes — origin binding
Offer a fallback to a code on the proxy pageNo — the authenticator is never invoked
Steal the session issued afterwardsNo
Trigger account recovery via the help deskNo
Act inside a stolen sessionNo

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

  1. Is the origin validated against an allow-list, or merely parsed? Parsing is not checking.
  2. Is rpIdHash compared, or assumed? Compare it.
  3. Is the challenge single-use, bound and time-limited? All three.
  4. Is user verification required, or accepted whatever its value? Require it.
  5. 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.

See step-up gating →

Sources and further reading