文章总结: 本文深入分析了AWSCognito在多SSO场景下的安全风险,揭示了恶意IdP注入幽灵用户、下划线拆分攻击和域名路由劫持等实际漏洞。文章详细解析了Cognito触发器时序和身份拼接逻辑,为开发人员提供了PreSignUp触发器安全闸门设置、用户名解析规范等具体防护建议,并发布了开源恶意IdP测试工具maSSO和可部署的实验环境。 综合评分: 87 文章分类: 云安全,漏洞分析,解决方案,安全工具,实战经验
别在 AWS Cognito 里随便玩多 SSO,这坑太多了
幻泉之洲
2026年5月18日 09:17 北京
在小说阅读器读本章
去阅读
多租户 SaaS 平台上为每个租户接入外部 IdP(多 SSO 用户池)已成惯例,但多数实现里藏着同样的安全陷阱。这篇文章拆解了 Cognito 在多 SSO 场景下的触发器时序、身份拼接逻辑和自注册 IdP 标识符带来的风险——恶意 IdP 注入幽灵用户、下划线拆分攻击、域名路由劫持,这些不是理论推演,是我们真实打穿过的模式。末尾附上一个开源的恶意 IdP 工具和一个可部署的漏洞实验环境。
CloudSecTidbits 系列又回来了。前阵子我们在新加坡 DEFCON DemoLabs 上讲了这部分内容,社区反馈不错——感谢招待。
前情提要
CloudSec Tidbits 这个系列记录的是 Doyensec 在云安全测试里挖到的有意思的漏洞。我们重点看那些因为 web 和云技术拼在一起没拼好产生的问题。每篇文章都附带一个基于 Terraform 的 IaC 实验环境[1],可以直接拉起来复现漏洞。
第四期,来看看多 SSO 用户池到底多危险。
AWS Cognito 的多 SSO 流程
如果对 Cognito 还不熟,可以先翻翻我们早前写的第一季第二集,关于用户属性篡改的[2]。这次我们抛开简单场景,直接看那种越来越普遍的多租户部署:一个 User Pool,一堆租户,每个租户带自己的外部 IdP(OIDC/SAML)。
开发者可以在一个用户池里注册多个外部 IdP,通过 Cognito 提供的托管 UI(那个统一的登录页面)暴露出去,或者自己写页面调用托管 SSO 端点。注册 IdP 用的是 CreateIdentityProvider API,一个最简的 OIDC 注册大概长这样:
这类注册动作通常是平台后端在租户配置自定义 IdP 时触发的。
新角色:AWS Lambda 触发器入门
触发器是同步钩子,开发者在事件驱动的流程里嵌入自定义逻辑。Cognito 在用户创建和 SSO 认证的特定阶段会调用多个触发器。这些触发器会暂停认证流程,让自定义逻辑去决定放行、拒绝或者修改请求。实际落地时,这些触发器承载了平台需要的“身份胶水”:域名白名单检查、所有权校验、租户限制、即时(JIT)供应、属性标准化、令牌塑造等等。
把各个触发器的执行顺序和事件类型画出来最清楚。下面这张图是我们做身份检查时常用的边界指南。
从安全角度看,有几个点值得关注:
- PreSignup 触发器是 Cognito 用户记录真正落地前唯一一道闸门。一旦身份进了池子,就可以通过平台其他功能跟它交互了。
- 首次联合登录和后续登录的触发器链路只在 TokenGeneration 这里交汇。如果你只在一侧链路上做了认证约束,那另一侧就可能被绕过。
- 用户一旦在池子里创建,没有自动回滚机制;清理必须人工搞。
- 联合登录不会触发其他自定义认证挑战、迁移用户、自定义消息或自定义发送者触发器。
如果 IdP 是恶意的?完整流程示例
下面看看当一个外部 OIDC IdP 参与时到底发生了什么。Cognito 会执行完整的 OIDC code flow,拿 /userinfo 端点,并根据创建时的配置合并声明。
恶意 IdP 可以从多个角度攻击依赖多 SSO Cognito 用户池的平台,具体取决于约束条件和嵌在里面的复杂身份逻辑。
现在所有条件齐了:一个额外的注入点(恶意 IdP 跟 Cognito 对话),加上一串复杂的触发器把各种身份约束粘在一起。下面说说可能引入漏洞的反模式。
1. 幽灵身份注入:有时落地就够了
前面说过,PreSignUp_ExternalProvider 是唯一那个在 Cognito 持久化用户记录之前触发的触发器。多数时候搞出一个幽灵身份很简单:
- 用自服务 SSO 配置页注册一个恶意 OIDC 服务器作为 IdP(比如 EvilCorp)。
- 用一个 [email protected] 邮箱发起联合登录。
- PreSignUp_ExternalProvider 触发,但没做域名检查,于是 Cognito 把用户记录落盘。
- PostConfirmation(JIT 供应 Lambda)触发,域名检查抛出异常,会话被阻断,但用户记录依然存在。PreAuthentication 虽然也配了同样的检查,但 SSO 不是跟这个用户交互的唯一方式。
就算后面有回滚逻辑把这条记录删了,中间这个操作窗口里,攻击者仍然可以利用平台其他功能跟这个已创建的身份对象交互。最坏情况包括强制重置密码获得非 SSO 登录能力、冒充用户直接拿到会话等等。
一个小提示:在其他字段的怪异转义和注入手法,可能会带来更多漏洞。始终要关注那些整体读取身份对象的组件。
2. 触发器源值:被遗忘的事件
Cognito 用多个 event.triggerSource 值区分创建路径和认证路径。这些 triggerSource 是传给自定义处理器的标识,告诉它是什么身份事件,该干什么。
这些值非常多,有些可能会被开发者遗漏或误读,引入漏洞。任何多 SSO 安全评审都要盯住这几个核心值:
| triggerSource | 触发时机 / 安全风险 | | — | — | | InboundFederation_ExternalProvider | 每次联合登录,不管新用户还是老用户,在用户记录落盘之前触发;如果跳过它,属性检查就落在 PreSignUp 上,而后者只在首次登录触发 | | PreSignUp_ExternalProvider | 首次联合登录将创建本地用户时触发;这里缺少身份检查会留下持久的幽灵身份 | | PreSignUp_AdminCreateUser | 通常在管理员/SCIM 创建路径触发 | | PostConfirmation_ConfirmSignUp | 确认后触发,包括首次联合登录的自动确认;无法阻止用户创建,只能操作已持久化的记录 | | PreAuthentication_Authentication | 仅在后续登录时触发;首次联合登录不触发,把检查只放在这里会导致首次登录不受保护 | | PostAuthentication_Authentication | 每次认证成功后触发,但无法阻断会话;只能做检测和审计挂钩,不是安全闸门 | | TokenGeneration_Authentication | 在 SDK/admin 认证时触发;来源与 HostedAuth 不同,施加在一侧的对应逻辑在另一侧实际上不存在 |
完整的所有 triggerSource 参考在这里:Lambda 触发器文档[3]
3. 联合用户名格式与下拆分攻击
Cognito 对联合用户的内部身份键不是邮箱,而是:
_
它在触发器里表现为 event.userName,在令牌里表现为 cognito:username。ProviderName 是池子里注册的 IdP 名称,sub 是 IdP 给出的主体标识(如果 IdP 是恶意的,sub 完全可控)。
Provider 碰撞:大小写和同形字
Cognito 对 ProviderName 只强制字节相等唯一性,但两个名字视觉相似而字节不同的 IdP 可以被注册到同一个池子里。比如:
| ProviderName | 易混淆码点 | 视觉呈现 | 备注 | | — | — | — | — | | LegitCorp | 无(ASCII) | LegitCorp | 基线,允许注册 | | LеgitCorp | е = U+0435(西里尔小写 ie) | LegitCorp | 同形“e”,允许注册于同一池 |
这很危险,因为多数面向人的地方看不出差异:托管 UI 按钮、审计日志、CLI 输出、grep 审计都只是渲染 Unicode 然后继续干活。更糟的是,一旦应用程序再搞个不一致的规范化(lower()、NFKC 之类),可能拆分出属于同一 IdP 的不同身份,或者查询解析到错误的记录。
sub 级别的拆分攻击
ProviderName 的正则禁止下划线。sub 声明可没有。于是完整的身份字符串可能包含多个下划线:
Corp_admin_override
如果组件 A 按 split(“_”, 1) 读取,组件 B 按 split(“_”)[-1](或其他位置索引)读取,同一个输入产生两种不同含义。
从恶意 IdP 发送 sub = [email protected],后果如下:
| Lambda | 代码 | 索引 | 看到的 | | — | — | — | — | | pre_signup(唯一性守卫) | sub.split(“_”)[1] | 第二个 token | “noise” 不在池中,放行 | | jit_provisioning(消费方) | sub.split(“_”)[-1] | 最后一个 token | “[email protected]”,存入 custom:primaryEmail |
4. IdP 标识符和路由劫持
IdP 标识符是 Cognito 做 IdP 重定向用的字符串。标准模式是邮箱域名路由:用户输入 [email protected],Cognito 查找 company.com,浏览器被重定向到拥有该标识符的 IdP。
控制了一个标识符,就实际控制了所有该标识符用户的初始重定向。所以,如果一个租户放弃或不注册某个标识符,空档就可能被另一个 IdP 抢注。AWS Cognito 本身不保证域名所有权,平台方必须确保在允许声明某个 idp-identifier 之前,该租户已确证其所有权。
这相当于经典的域名接管,后果严重。比如 gmail.com 在一个平台的自定义 IdP 配置里可以被任意认领,那你可能把所有 Google 用户都重定向到攻击者控制的页面。
别信任 IdP
多 SSO 改变了哪些触发器触发、应用把什么当作身份键、以及你有多少攻击者控制的字符串被意外解析成结构化数据。把控制点放错触发器会产生幽灵身份,用攻击者可控的 sub 值做解析可能产生提权,把 IdpIdentifiers 暴露为自服务字段则开了路由劫持窗口。
给云安全审计人员
审查基于 Cognito 的多租户平台时,回答这几个问题:
- 这个池子有没有注册外部 IdP?
- 每个 IdP 的 AttributeMapping 里映射了什么?如果 IdP 恶意或被攻破,那里面的东西都是攻击者可控的,跟 WriteAttributes 无关。
- PreSignUp Lambda 怎么分支 event.triggerSource?有没有覆盖 PreSignUp_ExternalProvider 和 PreSignUp_AdminCreateUser,而不只是 PreSignUp_SignUp?
- 所有身份检查是否在 JIT 和后续 SSO 登录两条触发器链路上都做了?没做到位的话很可能产生非预期身份创建。
- 有没有 Lambda 用类似 split(“_”) 加位置索引解析 event.userName 或 cognito:username?如果有,这个解析器在面对包含下划线的 sub 值时非常脆弱,要找守卫和消费方的差异。
- IdpIdentifiers 是否暴露在自服务 IdP 注册 UI 中?如果是,平台是否确保声明域名标识符的租户已证明其所有权?如果不是,未声明域名的任意重定向就可能发生。
- AttributeMapping 有没有映射安全敏感的的自定义属性(如 custom:tenantID, custom:role, custom:isAdmin)?就算 WriteAttributes 锁死,使用 AdminUpdateUserAttributes 的 JIT Lambda 仍然会写入它们。
给开发者
把安全闸门放在 PreSignUp 里,按 triggerSource 分支。这是对多 SSO 部署影响最大的单个改动。一个可行的模式:
def lambda_handler(event, context): if event[“triggerSource”] in ( “PreSignUp_SignUp”, “PreSignUp_ExternalProvider”, “PreSignUp_AdminCreateUser”, ): enforce_domain_policy(event[“request”][“userAttributes”][“email”]) return event
永远不要用 split(“_”) 拆 event.userName 来提取身份。如果必须解析,在所有地方都统一用 split(“_”, 1) (maxsplit=1)。守卫和消费方必须使用相同的提取逻辑,对攻击者可控字符串用位置索引就是等着被人打解析差异。
把安全相关的自定义属性从 AttributeMapping 里拿掉。tenantID 以及类似的字段应该在触发器内通过已验证的邮箱域名在服务端推算,不要从联合认证后的 event.request.userAttributes 里直接读。
在 PreSignUp 里严格校验邮箱。
至于 IdpIdentifiers:绝不要把它作为自服务 IdP 注册的自由文本字段暴露出去。在 IaC 里,原子化注册标识符。别在同一 apply 里先删后加。
工具发布:maSSO,干这个活儿的恶意 IdP
上面描述的几乎每一种滥用都基于同一个原始条件:一个服务提供方信任的、攻击者控制的 IdP,并且能够篡改发给它的令牌、SAML 断言和 /userinfo 负载。
专门为测试搭建自定义 IdP 太花时间,我们决定把自己渗透测试时用的那个放出来:doyensec/maSSO[4]。
maSSO 是一个武器化的、合规的单点登录(SSO)身份提供方(IdP),用于 OIDC 和 SAML 2.0 服务提供方的安全测试,也支持 SCIM 协议。
对我们而言,它就是测试 SP 的瑞士军刀。欢迎反馈。
可以动手的 IaC 实验环境
跟系列前言承诺的一样,我们准备了一个 Terraform 实验环境,可以部署一个有漏洞的虚拟应用来玩这些漏洞:https://github.com/doyensec/cloudsec-tidbits/tree/main/lab-masso
下期见。
参考资源
- 通过第三方添加用户池登录[5]
- Cognito 用户池 Lambda 触发器[6]
- Doyensec,篡改 AWS Cognito 中不受限制的用户属性(S1, Tidbit 2)(https://blog.doyensec.com/2023/01/24/tampering-unrestricted-user-attributes-aws-cognito.html)
- doyensec/maSSO,用于 SP 测试的武器化 OIDC/SAML IdP[4]
参考资料
[1] https://github.com/doyensec/cloudsec-tidbits/
[2] https://blog.doyensec.com/2023/01/24/tampering-unrestricted-user-attributes-aws-cognito.html
[3] https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-working-with-lambda-triggers.html
[4] https://github.com/doyensec/maSSO
[5] https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-identity-federation.html
[6] https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html
[7] https://blog.doyensec.com/2026/05/05/cloudsectidbits-masso-cognito-sso.html
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:幻泉之洲 《别在 AWS Cognito 里随便玩多 SSO,这坑太多了》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论