SAML:一个分形般的糟糕设计

SAML 在企业 IT 里活了二十多年,作者认为它每一层都建在沙子上:XML 签名验证、规范化、信封式签名、大杂烩式设计和协议僵化,每一样都反复出过漏洞。他逐条拆开,并说明为什么所有路最后都通向 OIDC。

中文
复制
深色封面,左上角是 Trail of Bits 标志,中间写着 AUTHENTICATION 与标题 SAML A fractal of bad design,下方署名 MATT SCHWAGER,右侧是一片灰红两色的像素网格图案

SAML(Security Assertion Markup Language,安全断言标记语言)认证协议诞生于学术界,在企业 IT 部门中成长,至今仍是这些组织里的常客。不过,它该退休了。随着 SaaS(软件即服务)公司在 2000 年代后期兴起,IT 部门需要一种让用户登录众多新 Web 服务的方式。SAML 和当时方兴未艾的单点登录(SSO)行业满足了这一需求。但 SAML 正被自身的复杂性压垮。是时候弃用它,转向 OpenID Connect(OIDC)这类现代替代方案了。本文将探讨 SAML 由委员会设计出来的起源、它在学术界和企业环境中一路攀升的过程、安全研究社区如何让它一点点瓦解,以及它(有望)被更新的协议取代而退场。

SAML 101

SAML 的阴险之处在于,它理解起来其实大多很直白,却建立在沙、骨灰和灰烬之上;它能用……前提是你假设 XML 签名验证是可靠的。但 XML 签名验证被诅咒得很深,复杂到大多数实际部署的 SAML 实现都在包装 libxmlsec——一个没人会去读的、晦涩难懂的 C 代码库。 —— Thomas Ptacek,2023

SAML 与 SSO 行业的诞生

Wikipedia 告诉我:“SAML 是一种基于 XML 的标记语言,用于安全断言。”它由 OASIS(结构化信息标准促进组织)安全服务技术委员会(SSTC)于 2002 年创建。好吧,按现代标准来看,这个开局不太妙。XML 尽管有一些可取之处,但相比 JSON 这类更新的替代方案要复杂得多,这一点后面再细说。更何况,一个由多个分委员会组成的委员会开会,正是“什么都往里塞”的协议设计的温床(比如瀑布式开发、前期大设计等)。果不其然,我们现在已经把四个(!)基于 XML 的安全协议塞进了一个:

……以下知识产权被贡献给了 SSTC:

  • Netegrity 的 Security Services Markup Language(S2ML)
  • Securant 的 AuthXML
  • VeriSign 的 XML Trust Assertion Service Specification(X-TASS)
  • Jamcracker 的 Information Technology Markup Language(ITML) —— SAML:历史

然而,对这样一种协议的渴望是毋庸置疑的。随着互联网从 90 年代的 Web 1.0 转向 21 世纪初的 Web 2.0,用户和组织需要一种简便的方式来对众多新兴的 Web 服务进行身份验证。学术界是这场运动最大的推动者,但并非唯一:2002 年耶鲁大学的 Central Authentication Service(CAS)、2003 年 Internet2(一个由研究型大学组成的联盟,包括我的母校)推出的 Shibboleth IdP、2003 年微软的 ADFS,以及 2007 年前后由 Uninett 推出的 simpleSAMLphp——Uninett 是一家与学术界关系密切的挪威国有企业。这些身份验证项目最终都以某种方式支持了 SAML。如同它之前的 ARPANET,大学站在互联网发展的最前沿,也是 Web 服务最早的消费者。一旦协议可用性这一基础层和初具雏形的学术试验场建立起来,商业产业便接手并将其推向了一个价值数十亿美元的行业。SSO、身份与身份验证提供商行业也在 21 世纪初起步,但真正开花结果是在几年之后:Ping Identity(2002)、OneLogin(2009)、Okta(2009)和 Duo Security(2010)。这些公司基本上都建立在 SAML 协议之上,Duo 是个例外——它直到 2015 年才推出自己的第一款 SSO 产品,而我的故事正是从这里开始的。我参与了 Duo 的第一款本地部署 Access Gateway 产品(DAG)的开发,它基于 simpleSAMLphp,当然也基于 SAML 协议。正是在那里,我对 SAML 协议变得了如指掌,并花了生命中许多年去消化它冗长的规范。Kelby Ludwig 发现 XML 注释绕过漏洞时我就在场,不过各种攻击手法和 SAML 的缺陷我们留到后面再谈。一句话,SSO 和身份验证提供商行业当时正蓬勃发展,而其中很大一部分都建立在 SAML 协议之上。

盔甲上的一道裂缝

XML 签名包装(XSW)攻击就是那支射向 SAML 阿喀琉斯之踵的箭。早年间已有针对签名包装(2005、2008、2009 年)和 SAML(2008 年)的安全研究,但在我看来,这一领域的开山之作是《On Breaking SAML: Be Whoever You Want to Be》(2012)。它用实践检验了理论,并催生出一套自动检测 XSW 攻击的方法。这也是我们实现 DAG 时的北极星,是我们选择 simpleSAMLphp 作为构建基础的原因。PHP 的安全性口碑向来不佳,在当时尤其如此,但 simpleSAMLphp 是个例外。在还没多少人知道 XSW 是什么的年代,simpleSAMLphp 就已经能抵御这类攻击:

simpleSAMLphp 的安全记录(来源:On Breaking SAML)

尽管 XSW 在 2012 年那篇论文里被摆到了台面上,它至今依然存在。既然我们已经知道这类漏洞,为什么还是修不掉?不过在讨论 SAML 的缺陷之前,得先看看它脚下那块并不牢靠的地基:XML。论安全记录的糟糕程度,XML 毫不逊色。这些漏洞类型对 90 年代的开发者来说或许更熟悉,但今天依然存在于 XML 之中:XXE、实体扩展(“billion laughs”)、DTD 获取(SSRF)、XPath/XQuery/XInclude/XSLT/CDATA 注入,等等。一个 SAML 库得先处理完所有这些漏洞类型,才轮得到实现真正的 SAML 功能。除了安全漏洞,XML 相比 JSON 这类格式还有 sheer 的复杂度。XML 里有标签、元素、属性、注释、命名空间、标记与内容的区分、schema、CDATA、DOCTYPE 等等;JSON 里基本上只有键、值、对象和列表。复杂度通常与安全相悖,这也是我认为 SAML 是糟糕设计的分形的原因之一。

糟糕设计的分形

SAML 提供了大量学习协议设计的机会。本节将讨论五个缺陷,我认为它们对 SAML 作为认证协议的长期可行性是致命的。这些缺陷在设计新的认证协议时同样有参考价值——你可以绕开它,也可以反其道而行之,把它反过来写进协议里。

建立在 XML 之上

如前所述,SAML 建立在 XML 之上,而 XML 很复杂,但这不是委员会的错。XML 是当时他们手上有的东西,也是人们在用的东西。JSON 在 2001 年被“发现”,但这恰好是 SAML 委员会开会的时期,他们不太可能围绕一种实验性的新格式来设计认证协议。尤其是这种格式迎合 JavaScript,而你写的是大量 Java。有人可以为 XML 与 JSON、或 SAML 与 JWT/OIDC 设计一种定量的复杂度度量(例如规范/RFC 字数、规范/RFC 规范性字数等),但那需要一篇专门的博客文章或论文。为了不离题,我在这里就不做了,只说一句:XML 比 JSON 之类的东西复杂得多。

规范化

规范化(C14N)就是当你想把 XML 那一团乱麻拿过来、计算它的哈希、并得到一致结果时所做的事。换句话说,如果 SP 和 IdP 无法就 XML 数据的一致表示达成一致,那么字节就对不上,签名就不匹配,你的认证就失败。然而,这事说起来容易做起来难。规范化漏洞促成了 Kelby 在 2018 年的 XML 注释绕过:

XML 规范化(来源:Identity Theft)

规范化往往是解析器差异和/或“往返”漏洞的前奏,而现代大多数 SAML 攻击用的正是这些:

  • Go 标准库中 XML 往返漏洞的协同披露(2020)
  • 保护 Web 各处的 XML 实现(2021)
  • 利用 libxml2 的怪癖绕过 GitHub Enterprise 上的 SAML 认证(2025)
  • 以任何人身份登录:用解析器差异绕过 SAML SSO 认证(2025)
  • SAML 轮盘赌:黑客总是赢(2025)
  • 脆弱的锁:SAML 认证的新型绕过(2025)

封装签名

封装签名的问题与规范化是关系不远的表亲。简而言之,如果你想把你正在签名的数据载荷里插入签名,那你会很难受。让我们这样比较和对照 JWT 与 SAML:

JWT 签名与 SAML 签名对比(图片来源:jwt.io 和 samltool.io)

在上面的 JWT 示例中,蓝色的签名与 JSON 载荷是分离的,在 JWT 中用句点(“.”)分隔。而在 SAML 示例中,Signature 元素被插入(“封装”)到 Assertion 元素内部。问题在于,当你同时还在修改数据时,很难得到逐字节、规范化等价的表示。面对 XML 这种复杂格式和复杂的规范化规则时,难度只会更大。

“大杂烩”式设计

这种设计缺陷本质上等同于“你并不需要它”(YAGNI)。这个说法并不完全公平,因为过去 20 年里,SAML 规范中实际被使用的部分已经发生了变化(抱歉了,SOAP 和 artifact binding)。但事实是,99% 的现代 SAML 实现只用到非常相似的数据结构和规范的一个子集。今天你在真实环境中遇到的任何一次 SAML 认证,大概率都绕开了规范的 90%。这为大量根本用不到的特性付出了可观的复杂度代价。

如果我要给一个新系统加 SAML 支持,除了所有标准的 SAML 检查之外,我还会考虑拒绝任何结构与 Okta、Onelogin、Google 或 Shib 生成的消息不一致的报文。 —— Thomas Ptacek,2021

僵化

SAML 诞生于另一个时代,为另一种需求而设计,此后一直没有得到必要的更新。这些问题通常是实际层面的,而非理论层面的。我所说的僵化大致可以列举如下:

  1. OIDC 假定使用 HTTP,而 SAML 与传输方式无关。 诚然,SAML 的 HTTP 绑定确实存在,也是最常用的方式,但它们并非必需。这给了 SAML 一定的灵活性,但也意味着这种灵活性必须有明确定义、必须在某处实现,而且可能藏有 bug。SAML 诞生之时,HTTP + TLS 尚未成为 Web 服务通信的绝对主干,而它从未与这一现代格局调和。HTTPS 让 OIDC 可以把加密的可信通信交给传输层处理。
  2. OIDC 通常假定网络拓扑是连通的,而 SAML 不假定。 最常见的 OIDC 流程(授权码)假定 OpenID Provider(OP)与 Relying Party(RP)可以直接通信(OIDC 的 OP/RP 即 SAML 的 IdP/SP)。诚然,带表单提交的 OIDC 隐式流程确实存在,但今天已经非常少见。此外,SAML 也有用于直接通信的 artifact binding,但同样非常少见。关键在于,如果 OP 和 RP 可以直接通信,认证响应载荷就不必承载做出认证和授权决策所需的全部信息。这减小了载荷的体积和复杂度。OIDC 的 OP 和 RP 可以通过后通道交换信息。
  3. OIDC 是多年来有机生长出来的,而 SAML 基本上是预先设计出来的。 OIDC 涵盖数十个历经多年有机生长的规范和 RFC。这些文档通常是为解决某个具体需求而创建的,而不是试图预判所有未来需求并提前构建。这类似于第一节中描述的敏捷方法论与瀑布模型之别。看看下面这份不完全的时间线:OpenID Connect 1.0 规范发布(2014)
  4. JOSE 技术栈通过 RFC 7515-7519 最终确定,覆盖 JW{S,E,K,A,T}(2015)
  5. PKCE 通过 RFC 7636 发布(2015)
  6. 面向移动/原生应用的 PKCE 通过 RFC 8252 发布(2017)
  7. 面向 IoT 设备的设备授权授予通过 RFC 8628 发布(2019)
  8. 面向 MFA 工作流的持有证明(DPoP)通过 RFC 9449 发布(2023)
  9. 面向 SPA 的 PKCE 通过 RFC 10017 发布(2026)

历史背景也很有意思。比如 SAML 成熟于 VPN 和网络分段的时代,这正是上面第 2 点的由来。如果 IdP 或 SP 位于企业防火墙之后,无法直接与另一端通信,那么整个部署就会卡住,厂商也就丢掉了这笔订单。SAML 必须无缝地处理这种情况。2014 年,Google 的 BeyondCorp 模型和零信任架构彻底颠覆了这一观念。此外,SAML 没有预见到移动端、SPA 和 IoT 革命,当这些技术在 2000 年代末登场时,它拿不出任何应对方案。尽管 SAML 在企业环境中仍被广泛采用,但这些宏观事件共同促成了该协议漫长而缓慢的衰落。敏捷性和松耦合让企业能够在不断变化的 IT 环境中快速适应。

条条大路通向 OIDC

没有哪个协议是完美的,但就解决方案而言,条条大路通向 OIDC。据我所知,SAML 唯一占优的部署场景,是 SP 和 IdP 无法直接通信的网络。OIDC 的隐式流配合表单 POST 能提供完全相同的要素。这实际上是我在业内见过的最干净的迁移方案之一。那么,如果我是一个服务提供方(即 SP),想接入 SSO 生态又不想用 SAML,该怎么办?支持 OIDC 就行。放弃 SAML。显然 Fly.io 和 Tailscale 已经在这么做了:

到目前为止,我们一直守住了 OIDC 这条线。Tailscale 也是。考虑到 Tailscale 的客户是谁,如果它能守住,我认为大多数组织也能。真的,尽量避免做 SAML。记住,作为厂商,你面对的竞争对手往往根本不做什么真正的 SSO 集成。 —— Thomas Ptacek,2024

那么,如果我是一个身份或认证提供方(即 IdP),想摆脱 SAML,该怎么办?嗯,这取决于你的客户数量,这条路可能确实很长。但你知道怎么吃掉一头大象吗?一口一口地吃。这在业内是一条走烂了的路:制定弃用计划,告知客户,停止让新客户接入 SAML 集成,为现有 SAML 客户提供等效的 OIDC 配置,设定终止日期,然后动手干。SAML 风光了 25 年。它催生了 SSO 行业,帮助保护了不计其数的认证,改善了登录数十种 Web 服务的用户体验,并创造了数十亿美元的经济影响。我们应该感谢 SAML 协议的创造者。它为我们提供了一个绝佳的案例,让我们得以研究在科技行业这段极为动荡的时期里,协议是如何设计与演进的。如果你想阅读更多安全话题的历史分析,可以看看《Marshal madness: A brief history of Ruby deserialization exploits》。如果你对协议设计审计感兴趣,或者想让我们审查你的认证系统,请联系我们。

来源: Trail of Bits← 返回首页