忘记时间的黑名单

发布: (2026年2月24日 GMT+8 14:40)
8 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的完整文本内容,我将按照要求保留源链接、格式和代码块,仅翻译正文部分为简体中文。

实际发生的情况

CVE‑2026‑27127 今日在 Craft CMS 中发布。它是一种通过 DNS 重绑定实现的高危 SSRF。 advisory(安全通报)看起来像标准的模板,但补丁说明中的一个细节引起了我的注意:此 CVE 绕过了 CVE‑2025‑68437——同一代码库中之前的 SSRF 修复。之前的修复已经发布,并得到渗透测试人员的签署认可,但攻击者仍然可以直接通过它。

这并不是一个简单的 bug——而是一次在安全评审中幸存的类别错误。

最初的修复添加了 IP 黑名单。在发起任何外部 HTTP 请求之前,Craft 会解析目标主机名并检查其 IP 是否在拒绝列表中(AWS 元数据 169.254.169.254、GCP、Azure、RFC 1918 范围、环回地址等)。如果 IP 在列表中,请求将被阻止。

合理。标准做法。错误。

漏洞逻辑(根据 advisory 重构)如下:

// Validation: DNS lookup #1
$ip = gethostbyname($hostname);
if (in_array($ip, $blocklist)) {
    return false; // blocked
}

// Request: DNS lookup #2 (inside Guzzle)
$response = $client->get($url);

这里发生了两次 DNS 查询:一次用于验证,另一次在 HTTP 库(Guzzle)内部进行。

如果攻击者控制一个 DNS 服务器,可以将其域名的 TTL=0。第一次查询返回一个安全的 IP,通过了黑名单检查。当 Guzzle 为实际请求再次解析同一主机名时,DNS 记录已经变为 169.254.169.254。请求会到达 AWS 元数据端点并泄露凭证。黑名单从未看到真实的目标地址。

这就是 TOCTOU 在 DNS 中的应用

时间检查/时间使用(Time‑of‑Check/Time‑of‑Use,TOCTOU)是最古老的漏洞类别之一。你在时间 T₁ 检查一个条件,在时间 T₂ 对其进行操作,而两者之间的状态可能已经改变。经典案例涉及文件系统竞争:先检查文件,然后打开它,但在这段间隙文件已经被替换。

DNS 重绑定(rebinding)就是在不同的底层实现上出现的同类漏洞。被检查的条件是“这个主机名是否解析为安全的 IP?”对应的操作是 HTTP 请求。当攻击者控制 DNS 服务器并能够对不同查询返回不同答案时,这段间隙就可以被利用。

TTL=0 时,重绑定几乎是瞬时的。没有缓存可以抵消。攻击窗口只有微秒到毫秒级——虽然窗口很短,但只要有配合的 DNS 服务器,就能可靠利用。

类似的模式自 2019 年起就在漏洞赏金报告中出现(例如某 Python webhook 服务泄露 AWS 密钥),以及 CVE‑2024‑28224(Ollama)中也有体现。生态系统不断重现这种错误,因为修复方案看起来“正确”:检查 IP,就认为安全。

为什么 “更好的阻止列表” 并不起作用

看到这个 bug 后的直觉是扩大阻止列表:添加更多范围、进行第二次验证等。但这方向是错误的。

无论阻止列表多么全面,都无法解决结构性问题。主机名会被解析两次——一次由你控制,另一次由 HTTP 库自行解析。只要这两次解析是分开的,拥有 DNS 控制权的攻击者就可以对每个查询返回不同的答案。

即使你的阻止列表覆盖了所有云元数据范围、所有私有 IP 和所有回环地址,攻击者的 DNS 服务器仍然可以:

  1. 为验证查询返回一个安全的 IP。
  2. 为库的查询返回一个恶意的 IP(例如元数据服务)。

阻止列表检查的是与实际重要的解析不同的那一次解析。

在架构层面修复

Craft 补丁使用 CURLOPT_RESOLVE,这是 libcurl 的一个选项,用于在一次请求期间将主机名固定到特定的 IP。其流程如下:

  1. 解析一次主机名。
  2. 将得到的 IP 与阻断列表进行校验。
  3. 告诉 curl:“对于这个主机名,使用 IP;不要再解析。”
  4. 发起请求。

只会进行一次解析,库随后再也不会执行 DNS 查询。

另一种做法是将 URL 重写为直接使用 IP,并在请求中通过 Host 头部传递原始主机名。原理相同:你控制请求实际访问的 IP,而不是依赖 DNS 两次返回相同的答案。

始终能够正确实现该目标的模式是:

在信任边界进行解析,验证,然后固定。
永远不要把主机名交回会自行解析的库。近期的 SSRF 防护库都采用了这一设计决策,将 DNS 解析视为在外围进行的一次性操作。

让我困扰的部分

2025 年的修复是一次真正的尝试。有人发现了缺失的验证,编写了阻止列表并将其发布。安全审查大概已经进行并通过。

“这个 IP 看起来安全吗?”是错误的问题。正确的问题是“请求实际上会发送到这个 IP 吗?”只有在解析一次并固定后,这两个问题才等价。2025 年的修复回答了错误的问题——虽然做得很专业——却忽略了 HTTP 请求期间 DNS 的底层模型。

如果你在自己的代码中实现 SSRF 防护,尤其是使用内部处理 DNS 的请求库时,请自问:

  • 我是否只解析了一次主机名?
  • 我是否确保库在实际请求时使用了同一次解析的结果?

如果你无法对这两个问题都回答“是”,那么你的阻止列表仅是装饰品。

Craft CMS 修复已在 4.16.19 和 5.8.23 中发布。如果你运行的是启用了 GraphQL 资产创建的自托管实例,请立即更新。

0 浏览
Back to Blog

相关文章

阅读更多 »

没人想负责的 Systemd Bug

TL;DR:存在一个命名空间 bug,影响 Ubuntu 20.04、22.04 和 24.04 服务器,导致随机服务故障。自 2021 年起已在系统中报告……