← All posts

身份提供方告诉我们你是谁,而我们信了

Authagonal·July 6, 2026
authsecurityssosamloidcfederation

单点登录有一个不言而喻的前提:当一个用户从某个身份提供方而来时,该提供方已经为他作过担保,所以你可以跳过密码,直接放他进来。其全部意义就在于信任 IdP。我们接下来要描述的这个漏洞,就藏在"信任 IdP 去认证它自己的用户"和"信任 IdP 告诉你的关于它的用户是谁的一切"这两者之间的缝隙里。这并不是同一句话,而二者的差别,便是一次账户接管。

事情是这样的。我们平台上的一个租户可以配置联合连接:用 SAML 连到它的企业 IdP,用 OIDC 连到另一个。一个用户通过其中一个连接登录,IdP 回传一份断言,而我们的服务器必须回答一个问题:这是哪一个本地账户?把这个映射搞错,你要么得到一个被锁在自己账户之外的陌生人,要么——糟糕得多——得到一个大摇大摆走进别人账户的陌生人。

连接键是一个电子邮件,而电子邮件不过是一项声明

我们的代码用最显而易见的方式回答了"这是哪个账户"。断言里带着一个电子邮件,于是我们就按它来查找用户:FindByEmailAsync(assertedEmail)。如果存在一个匹配的账户,回访的联合用户就被解析到它上面并登录。干净、简单,而且正是许多 SSO 集成的写法。

问题在于那个电子邮件实际上是什么。它并不是 IdP 证明过的事实。它是断言里的一个字符串,而断言的可信程度,只取决于它所经由的那个连接的可信程度。在多租户的世界里,你并不掌控每一个连接。任何能搭建起一个连接的人(他自己的 SAML IdP、他自己的 OIDC 提供方)都可以往电子邮件字段里塞进任意他想要的字符串。于是一个你并不信任的连接声称 email = [email protected],我们的查找找到了真正的 CEO 的账户,攻击者便以其身份登录了。没有密码,没有钓鱼,密码学上也没有任何缺陷。断言上的签名完全有效。它只不过为一项我们本就绝不该当作身份来对待的声明作了担保。

令人不安的地方在于,每一个单独的环节都运转正常。IdP 正确地认证了它自己的用户。签名通过了验证。账户确实存在。这次接管并非任何一项检查的失败;它只是把错误的字段当成了键。

为什么更多的校验并非修复之道

诱人的反应是把电子邮件校验得更严。要求 email_verified。检查域名。加一条规则。但请留意这个陷阱的形状:攻击者掌控着断言,所以你从断言里读出的任何属性,都是攻击者能够设定的属性。向一个攻击者自己拥有的连接索要 email_verified = true,无异于请狐狸来为鸡舍做担保。你无法靠校验来摆脱"信任了错误来源"这件事。

电子邮件作为一种便利是没问题的。它是一个糟糕透顶的主键,因为它是全局的(同一个地址可以出现在许多提供方那里),又是可伪造的(它就是那个声称它的连接所说的任何东西)。一个连接键,这两样都不能沾。

修复之道是换键,而非添加检查

真正的修复,是彻底不再以电子邮件来连接,而改用一个连接对于它并不拥有的提供方无法伪造的东西:(provider, sub) 这一对。sub 是提供方为该用户签发的主体标识符,其作用域限定在那个提供方之内。一个回访用户的解析,是去查找此前与连接的 (provider, sub) 关联过的那个本地身份。攻击者的连接有它自己的提供方 id;它可以在自己的命名空间里铸造任意它喜欢的 sub,却无法产出属于别人那个连接的 (provider, sub)。命名空间就是那道护城河。

电子邮件于是退回到它本就该一直担任的角色:一条用于关联的线索,绝不用于解析,而且只在它有担保的时候。我们只有在电子邮件经由一个我们对那个域名确实信任的来源验证过时,才把一个被声称的电子邮件匹配到一个既有账户上:来自某个连接的 email_verified,且该连接的域名在租户的 AllowedDomains 之中。在此之外,一个新的联合身份会得到它自己的账户,而不是别人的。

抽离出来的教训

当你联合身份时,请把两个你忍不住想合二为一的问题分开。"这个提供方认证过这个用户吗?"由签名来回答。"这个用户在我的系统里是谁?"则必须由一个断言方无法跨越边界伪造的键来回答,这意味着一个按提供方划分的 subject,而不是像电子邮件那样的全局属性。请把断言中的每一个字段都当作受攻击者控制,除非那个字段的具体来源,正是你为那一项具体声明所信任的来源。电子邮件是人们最先去抓的那个字段,因为它有人情味,又看起来独一无二。它恰恰是错的那一个。

我们是在一轮发布前的安全梳理中,针对我们自己的认证服务器交付了这项改动,那时还没有任何人把他们的登录托付给我们。这正是那种通过了每一项测试、验证了每一个签名、却把钥匙交给任何以正确格式开口索要之人的漏洞。修复之道并不是一把更好的锁。它是意识到,我们一直读错了那张身份证件上的那一行。

安全地联合身份,归根结底就是以一个断言方无法伪造的键来连接,而把这一步做错,便是一次能通过每一项测试的账户接管。Authagonal 按每个提供方划分的 subject 来解析联合用户,绝不按断言中的电子邮件。