Sign in as anyone: Bypassing SAML SSO authentication with parser differentials
任意用户登录:利用解析器差异绕过 SAML SSO 认证
关键的认证绕过漏洞 (CVE-2025-25291 + CVE-2025-25292) 在 1.17.0 及更早版本的 ruby-saml 中被发现。在这篇博文中,我们将重点介绍如何发现这些依赖于解析器差异的漏洞。

Peter Stöckli·@p- 2025年3月12日 | 12 分钟
关键的身份验证绕过漏洞(CVE-2025-25291 + CVE-2025-25292)在 1.17.0 之前的
ruby-saml版本中被发现。拥有使用验证目标组织SAML响应或断言的密钥创建的单个有效签名的攻击者可以使用它来构造自己的SAML断言,进而能够以任何用户身份登录。换句话说,它可以用于帐户接管攻击。ruby-saml的用户应更新到 1.18.0 版本。对使用ruby-saml的库的引用(例如omniauth-saml)也需要更新到引用ruby-saml的固定版本的版本。
在这篇博文中,我们将详细介绍在服务提供商 (应用程序) 端通过 SAML 进行单点登录 (SSO) 使用的 ruby-saml 库中新发现的身份验证绕过漏洞。GitHub 目前未使用 ruby-saml 进行身份验证,但已开始评估该库的使用情况,目的是再次使用开源库进行 SAML 身份验证。但是,此库在其他流行的项目和产品中使用。我们在 GitLab 中发现了一个可利用的此漏洞实例,并已通知他们的安全团队,以便他们采取必要的措施来保护其用户免受潜在的攻击。
GitHub 之前在 2014 年之前使用了 ruby-saml 库,但由于当时 ruby-saml 中缺少功能而转移到我们自己的 SAML 实现。在围绕我们自己的实现中的漏洞收到漏洞赏金报告 (例如 CVE-2024-9487,与加密断言有关) 之后,GitHub 最近决定再次探索使用 ruby-saml。然后在 2024 年 10 月,一个重磅炸弹漏洞出现了:ruby-saml (ahacker1) 中的一个身份验证绕过 (GHSA-jw9c-mfg7-9rx2) (CVE-2024-45409)。有了可利用的攻击面的切实证据,GitHub 切换到 ruby-saml 现在必须进行更彻底的评估。因此,GitHub 启动了一个私人漏洞赏金参与来评估 ruby-saml 库的安全性。我们让选定的漏洞赏金研究人员可以访问使用 ruby-saml 进行 SAML 身份验证的 GitHub 测试环境。同时,GitHub 安全实验室还审查了 ruby-saml 库的攻击面。
正如多个研究人员查看同一代码时经常发生的情况一样,GitHub 漏洞赏金计划的参与者 ahacker1 和我都在代码审查期间注意到了同一件事:ruby-saml 在签名验证的代码路径中使用了两个不同的 XML 解析器。即 REXML 和 Nokogiri。虽然 REXML 是用纯 Ruby 实现的 XML 解析器,但 Nokogiri 围绕不同的库(如 libxml2、libgumbo 和 Xerces(用于 JRuby))提供了一个易于使用的包装器 API。Nokogiri 支持解析 XML 和 HTML。看起来 Nokogiri 已被添加到 ruby-saml 以支持 规范化 以及当时 REXML 可能不支持的其他内容。
我们都检查了 xml_security.rb 的 validate_signature 方法中的相同代码路径,发现要验证的签名元素首先通过 REXML 读取,然后也通过 Nokogiri 的 XML 解析器读取。因此,如果可以诱使 REXML 和 Nokogiri 为同一 XPath 查询检索不同的签名元素,则有可能诱使 ruby-saml 验证错误的签名。看起来由于解析器差异可能存在潜在的身份验证绕过!
实际上,现实比这复杂得多。
当不同的解析器以不同的方式解释相同的输入时,就会发生解析器差异。还有其他记录在案的解析 XML 时导致安全影响的解析器差异示例:
- Ivan Fratric 的“
XMPP消息段走私或我如何破解Zoom” - Siguza 的“
Psychic paper”
但是,解析器差异远比这更常见,并且绝不仅限于文件格式。其他类型的漏洞通常在其核心具有解析器差异。例如,某些服务器端请求伪造 (SSRF) 漏洞中如何解析 URL,或者请求走私攻击中如何解释 HTTP 标头。
LangSec 论文“Ali 和 Smith 的 解析器差异反模式调查”根据其根本原因对真实世界的解析器差异进行了分类。
粗略地说,发现此身份验证绕过涉及四个阶段:
- 在代码审查期间发现使用了两个不同的
XML解析器。 - 确定是否以及如何利用解析器差异。
- 找到正在使用的解析器的实际解析器差异。
- 利用解析器差异创建完整的漏洞利用。
为了证明此漏洞的安全影响,有必要完成所有四个阶段并创建完整的身份验证绕过漏洞利用。
快速回顾:如何验证 SAML 响应
安全断言标记语言 (SAML) 响应用于以 XML 格式将有关已登录用户的信息从身份提供商 (IdP) 传输到服务提供商 (SP)。通常,传输的唯一重要信息是用户名或电子邮件地址。当使用 HTTP POST 绑定时,SAML 响应通过最终用户的浏览器从 IdP 传输到 SP。这清楚地表明为什么必须进行某种签名验证以防止用户篡改消息。
让我们快速看一下简化的 SAML 响应是什么样的:

注意:在上面的响应中,为了更好地阅读,删除了 XML 命名空间。
您可能已经注意到:简单 SAML 响应的主要部分是其断言元素 (A),而断言中包含的主要信息是 Subject 元素 (B) 中包含的信息(此处包含用户名:admin 的 NameID)。真实的断言通常包含更多信息(例如,作为 Conditions 元素一部分的 NotBefore 和 NotOnOrAfter 日期。)
通常,Assertion (A)(没有整个 Signature 部分)被规范化,然后与 DigestValue (C) 进行比较,并且 SignedInfo (D) 被规范化并针对 SignatureValue (E) 进行验证。在此示例中,SAML 响应的断言已签名,而在其他情况下,整个 SAML 响应已签名。
搜索解析器差异
我们了解到 ruby-saml 使用两个不同的 XML 解析器(REXML 和 Nokogiri)来验证 SAML 响应。现在让我们看一下签名验证和摘要比较。以下解释的重点在于 xml_security.rb 内部的 validate_signature 方法。
在该方法内部,REXML 中有一个广泛的 XPath 查询,用于查找 SAML 文档中的第一个签名元素:
sig_element = REXML::XPath.first(
@working_copy,
"//ds:Signature",
{"ds"=>DSIG}
)
提示:在阅读代码片段时,您可以通过查看它们的调用方式来区分 REXML 和 Nokogiri 的查询。REXML 方法以 REXML:: 为前缀,而 Nokogiri 方法在 document 上调用。
稍后,实际的 SignatureValue 从此元素中读取:
base64_signature = REXML::XPath.first(
sig_element,
"./ds:SignatureValue",
{"ds" => DSIG}
)
signature = Base64.decode64(OneLogin::RubySaml::Utils.element_text(base64_signature))
注意:Signature 元素的名称可能有点令人困惑。虽然它包含 SignatureValue 节点中的实际签名,但它也包含 SignedInfo 节点中实际签名的部分。最重要的是,DigestValue 元素包含断言的摘要(哈希)以及有关所用密钥的信息。
因此,实际的 Signature 元素可能如下所示(为更好地阅读而删除了命名空间信息):
<Signature>
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<Reference URI="#_SAMEID">
<Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" /></Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<DigestValue>Su4v[..]</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>L8/i[..]</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>MIID[..]</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
稍后在同一方法(validate_signature)中,再次查询 Signature(s) — 但这次使用 Nokogiri。
noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
然后,从该签名中获取 SignedInfo 元素并进行规范化:
noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
让我们记住此 canon_string 包含规范化的 SignedInfo 元素。
然后,SignedInfo 元素也使用 REXML 提取:
signed_info_element = REXML::XPath.first(
sig_element,
"./ds:SignedInfo",
{ "ds" => DSIG }
)
从此 SignedInfo 元素中,读取 Reference 节点:
ref = REXML::XPath.first(signed_info_element, "./ds:Reference", {"ds"=>DSIG})
现在,代码通过使用 Nokogiri 查找具有签名元素 id 的节点来查询引用的节点:
reference_nodes = document.xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
extract_signed_element_id 方法借助 REXML 提取 签名元素 id。根据之前的身份验证绕过 (CVE-2024-45409),现在有一个检查,即只能存在一个具有相同 ID 的元素。
采用第一个 reference_nodes 并进行规范化:
hashed_element = reference_nodes[0][..]canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
然后 canon_hashed_element 被哈希:
hash = digest_algorithm.digest(canon_hashed_element)
然后,使用 REXML 提取用于与其进行比较的 DigestValue:
encoded_digest_value = REXML::XPath.first(
ref,
"./ds:DigestValue",
{ "ds" => DSIG }
)
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
最后,将 hash(从 Nokogiri 提取的元素构建)与 digest_value(使用 REXML 提取)进行比较:
unless digests_match?(hash, digest_value)
几行前提取的 canon_string(Nokogiri 提取的结果)稍后针对 signature(使用 REXML 提取)进行验证。
unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
最后,我们有以下星座:
- 断言使用
Nokogiri提取和规范化,然后进行哈希处理。相反,将与其进行比较的哈希值使用REXML提取。 SignedInfo元素使用Nokogiri提取和规范化 - 然后针对使用REXML提取的SignatureValue进行验证。
利用解析器差异
问题是:是否可以创建 XML 文档,其中 REXML 看到一个签名,而 Nokogiri 看到另一个签名?
事实证明,是的。
参与漏洞赏金的 Ahacker1 更快地生成了使用解析器差异的工作漏洞利用。其中,ahacker1 受 Mattermost 的 Juho Forsén 在 2021 年发布的 XML 往返漏洞的启发。
不久之后,我在 Trail of Bits 的 Ruby fuzzer(称为 ruzzy)的帮助下,使用不同的解析器差异制作了一个漏洞利用。
两种漏洞利用都会导致身份验证绕过。这意味着拥有使用用于验证目标组织 SAML 响应或断言的密钥创建的单个有效签名的攻击者可以使用它来构造任何用户的断言,这些断言将被 ruby-saml 接受。这样的签名可能来自另一个(非特权)用户的签名断言或响应,或者在某些情况下,甚至可能来自 SAML 身份提供商的签名元数据(可以公开访问)。
漏洞利用可能如下所示。这里,添加了一个额外的签名作为 StatusDetail 元素的一部分,该元素仅对 Nokogiri 可见:

总之:
对 Nokogiri 可见的签名的 SignedInfo 元素 (A) 经过规范化,并针对从 REXML 看到的签名中提取的 SignatureValue (B) 进行验证。
通过查找其 ID,通过 Nokogiri 检索断言。然后,此断言经过规范化和哈希处理 (C)。然后,将哈希与 DigestValue (D) 中包含的哈希进行比较。此 DigestValue 通过 REXML 检索。此 DigestValue 没有对应的签名。
因此,发生了两件事:
- 有效的
SignedInfo带有DigestValue,针对有效的签名进行验证。(这会检查出来) - 伪造的规范化断言与其计算的摘要进行比较。(这也会检查出来)
这允许拥有任何(非特权)用户的有效签名断言的攻击者伪造断言,从而冒充任何其他用户。
使用 Nokogiri 时检查错误
通过检查 SAML 响应中的 Nokogiri 解析错误,可以停止当前已知的未公开漏洞利用的部分内容。遗憾的是,这些错误不会导致异常,而是需要在已解析文档的 errors 成员中进行检查:
doc = Nokogiri::XML(xml) do |config|
config.options = Nokogiri::XML::ParseOptions::STRICT | Nokogiri::XML::ParseOptions::NONET
end
raise "XML errors when parsing: " + doc.errors.to_s if doc.errors.any?
虽然这远非是对当前问题的完美修复,但它至少使一种漏洞利用变得不可行。
泄露指标
我们不知道任何可靠的泄露指标。虽然我们发现了一个潜在的泄露指标,但它仅在类似调试的环境中有效,并且要发布它,我们将不得不透露太多关于如何实现工作漏洞利用的细节,因此我们决定最好不要发布它。相反,我们最好的建议是在服务提供商端查找来自与用户的预期位置不符的 IP 地址的可疑 SAML 登录。
SAML 和 XML 签名:令人困惑的是
有些人可能会说将系统与 SAML 集成很困难。这可能是真的。但是,以安全的方式使用 XML 签名编写 SAML 的实现更加困难。正如其他人之前所说:最好无视规范,因为遵循它们无助于构建安全的实现。为了重新说明如果对 SAML 断言进行签名,验证是如何工作的,让我们看一下下面的图,该图描述了一个简化的 SAML 响应。断言(传输受保护的信息)包含一个签名。令人困惑,对吗?

更复杂的是:这里甚至签署了什么?整个断言?不!
签名的是 SignedInfo 元素,SignedInfo 元素包含 DigestValue。此 DigestValue 是规范化断言的哈希值,在规范化之前删除了签名元素。这种两阶段的验证过程可能导致哈希验证和签名验证之间存在脱节的实现。对于这些 Ruby-SAML 解析器差异就是这种情况:虽然哈希和签名本身都经过检查,但它们没有连接。哈希实际上是断言的哈希,但签名是包含另一个哈希的另一个 SignedInfo 元素的签名。您实际想要的是哈希内容、哈希和签名之间的直接连接。(一旦完成验证,您只想从实际验证的精确部分检索信息。)或者,使用不太复杂的标准在两个系统之间传输加密签名的用户名 - 但我们就在这里。
在这种情况下,该库已经提取了 SignedInfo 并使用它来验证其规范化字符串 canon_string 的签名。但是,它没有使用它来获取摘要值。如果该库使用了已经提取的 SignedInfo 的内容来获取摘要值,那么即使使用了两个 XML 解析器,它在这种情况下也是安全的。
结论
正如再次表明的那样:在安全上下文中依赖两个不同的解析器可能很棘手且容易出错。也就是说:在这种情况下,可利用性并不能自动保证。正如我们在此案例中所看到的,检查 Nokogiri 错误无法阻止解析器差异,但至少可以阻止对其进行一种实际的利用。
身份验证绕过的初始修复不会删除 XML 解析器之一以防止 API 兼容性问题。如前所述,更根本的问题是哈希验证和签名验证之间的脱节,可以通过解析器差异进行利用。删除 XML 解析器之一已计划用于其他原因,并且可能会作为主要版本的一部分出现,并结合额外的改进以加强该库。如果您的公司依赖开源软件来实现关键业务功能,请考虑赞助他们以帮助资助他们未来的开发和错误修复版本。
如果您是 ruby-saml 库的用户,请确保更新到包含 CVE-2025-25291 和 CVE-2025-25292 修复的最新版本 1.18.0。对使用 ruby-saml 的库的引用(例如 omniauth-saml)也需要更新到引用 ruby-saml 的固定版本的版本。我们将在稍后在 GitHub 安全实验室存储库中发布漏洞利用的概念验证。
致谢
特别感谢 ruby-saml 的维护者 Sixto Martín 和 GitHub 漏洞赏金计划的 Jeff Guerra。还要特别感谢 ahacker1 对这篇博文的投入。
时间线
- 2024-11-04:针对评估
ruby-saml进行SAML身份验证的GitHub测试环境报告了演示身份验证绕过的漏洞赏金报告。 - 2024-11-04:开始识别和测试潜在的缓解措施。
- 2024-11-12:Peter 发现的第二个身份验证绕过使计划用于第一个的缓解措施变得毫无用处。
- 2024-11-13:与
ruby-saml的维护者 Sixto Martín 的初步联系。 - 2024-11-14:两个解析器差异都报告给
ruby-saml,维护者立即响应。 - 2024-11-14:维护者和 ahacker1 开始开发潜在的补丁。(最初的想法之一是删除
XML解析器之一,但这在不破坏向后兼容性的情况下是不可行的)。 - 2025-02-04:ahacker1 提出了一个非向后兼容的修复。
- 2025-02-06:ahacker1 还提出了一个向后兼容的修复。
- 2025-02-12:
GitHub安全实验室公告的 90 天截止日期结束。 - 2025-02-16:维护者开始开发一个修复程序,其想法是向后兼容且更易于理解。
- 2025-02-17:与
GitLab的初步联系,以协调其本地产品与ruby-saml库的发布。 - 2025-03-12:发布了
ruby-saml的固定版本。
标签:
- 身份验证
GitHub安全实验室- [ruby](https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/<https