XSS Is Deadly for Passkeys: The Hidden Risk of Attestation None

XSS Is Deadly for Passkeys: The Hidden Risk of Attestation None

XSS 对通行密钥(Passkeys)是致命的:Attestation None 带来的隐蔽风险

A single XSS vulnerability can turn passkeys from a phishing-resistant login mechanism into a persistent account takeover backdoor. If malicious JavaScript can run on your page, it may be able to register an attacker-controlled passkey against the victim’s account. The user sees nothing, the website records a successful registration, and the attacker walks away with a valid authentication backdoor. 一个简单的 XSS 漏洞就能将通行密钥(Passkeys)从一种防钓鱼的登录机制,变成一个持久化的账户接管后门。如果恶意 JavaScript 能够在你的页面上运行,它就有可能为受害者的账户注册一个由攻击者控制的通行密钥。用户对此一无所知,网站记录了一次成功的注册,而攻击者则获得了一个有效的身份验证后门。

For an organisation, that means more than “someone found XSS”. It means identity compromise, persistence, audit-trail ambiguity, regulatory exposure, and a security control that appears to have worked while silently enabling an attacker. 对于企业而言,这意味着的不仅仅是“有人发现了 XSS”。它意味着身份泄露、持久化威胁、审计追踪模糊、合规风险,以及一种看似有效但实际上在暗中为攻击者提供便利的安全控制措施。

The uncomfortable truth is that while passkeys do bring amazing benefits, and I think that everyone should use them, there is a dangerous gap in the threat model that’s being overlooked by almost everyone I speak to. This blog post explains the risk, demonstrates how this is possible, and what the effective defences look like. 一个令人不安的事实是,虽然通行密钥确实带来了巨大的好处,我也认为每个人都应该使用它们,但在威胁模型中存在一个危险的漏洞,而我交谈过的几乎每个人都忽略了这一点。这篇博文将解释这种风险,演示它是如何发生的,并说明有效的防御措施是什么样的。

Introduction

简介

Before we get started, if you’d like a brief overview of how passkeys work, you can jump over to my Passkeys 101 blog post, where I explain the basics. I’m going to assume in this blog post that you understand the concept of passkeys, and we’re going to look at how they work in more detail in this post. 在开始之前,如果你想简要了解通行密钥的工作原理,可以跳转到我的《Passkeys 101》博文,我在那里解释了基础知识。我假设你在阅读本文时已经理解了通行密钥的概念,我们将在这篇文章中更详细地探讨它们的工作方式。

We also need to establish some terminology to make the rest of this blog post easier to understand: 我们还需要确立一些术语,以便更容易理解本文的后续内容:

  • Relying Party (RP): The website or application that stores and verifies a user’s passkey credential for authentication. 依赖方 (Relying Party, RP): 存储并验证用户通行密钥凭据以进行身份验证的网站或应用程序。
  • Authenticator: The user’s device or password manager that creates, stores, and uses the private key to prove the user’s identity to the Relying Party. 验证器 (Authenticator): 用户的设备或密码管理器,负责创建、存储并使用私钥向依赖方证明用户身份。
  • Attestation: The mechanism an Authenticator can use during registration to prove what kind of hardware created the credential. 证明 (Attestation): 验证器在注册过程中可以用来证明凭据是由何种硬件创建的机制。

How Passkey Registration Works

通行密钥注册的工作原理

When registering a passkey with an RP like Report URI, JavaScript will make a call out to fetch the data it needs: 当在像 Report URI 这样的依赖方注册通行密钥时,JavaScript 会发起调用以获取所需数据:

const optRes = await fetch('/passkeys/register_get_options/' + getCsrfToken(), { method: 'POST' });

The RP will return a response that looks like this and contains the publicKey object: 依赖方将返回如下响应,其中包含 publicKey 对象:

{
  "publicKey": {
    "rp": { "name": "Report URI", "id": "report-uri.com" },
    "user": { "id": "...", "name": "jane@example.com" },
    "challenge": "...",
    "authenticatorSelection": {
      "requireResidentKey": true,
      "residentKey": "required",
      "userVerification": "required"
    },
    "attestation": "none"
  }
}

Now that your device has the information it needs, it can create the new passkey and save it, likely showing you some kind of confirmation that requires a PIN, FaceID, TouchID, etc… This is done with the following JavaScript API call that will trigger the interaction with your Authenticator: 现在你的设备已经拥有了所需的信息,它可以创建并保存新的通行密钥,通常会向你显示某种需要 PIN 码、FaceID、TouchID 等验证的确认界面。这是通过以下 JavaScript API 调用完成的,它将触发与验证器的交互:

const cred = await navigator.credentials.create({ publicKey });

If you complete the process, your Authenticator will then store your new passkey. The JavaScript will then build the response to send back to the RP to confirm that everything has been completed and to save the new passkey against the user’s account: 如果你完成了该过程,验证器将存储你的新通行密钥。随后,JavaScript 会构建响应发送回依赖方,以确认一切已完成,并将新通行密钥保存到用户账户下:

const payload = { 
  id: cred.id, 
  clientDataJSON: cred.response.clientDataJSON, 
  attestationObject: cred.response.attestationObject 
};
// ... fetch to /passkeys/register_finish/

The attestationObject contains the important information, with everything else being mostly metadata. attestationObject 包含重要信息,其余部分大多是元数据。

The RP can now save the public key against the user and we know that this is a passkey they will be able to use to authenticate in the future. 依赖方现在可以将公钥保存到用户账户下,我们知道这就是用户将来可以用来进行身份验证的通行密钥。

How Passkey Authentication Works

通行密钥身份验证的工作原理

The process for logging in is equally as simple, with only a couple of steps to successfully authenticate with a passkey. First, the JavaScript must fetch the information required to authenticate from the RP. 登录过程同样简单,只需几个步骤即可成功通过通行密钥进行身份验证。首先,JavaScript 必须从依赖方获取身份验证所需的信息。

const optRes = await fetch('/passkeys/login_get_options/' + getCsrfToken(), { method: 'POST' });

The RP will respond with a publicKey object that contains the required information. 依赖方将返回一个包含所需信息的 publicKey 对象。