忘记时间的黑名单
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 服务器仍然可以:
- 为验证查询返回一个安全的 IP。
- 为库的查询返回一个恶意的 IP(例如元数据服务)。
阻止列表检查的是与实际重要的解析不同的那一次解析。
在架构层面修复
Craft 补丁使用 CURLOPT_RESOLVE,这是 libcurl 的一个选项,用于在一次请求期间将主机名固定到特定的 IP。其流程如下:
- 解析一次主机名。
- 将得到的 IP 与阻断列表进行校验。
- 告诉 curl:“对于这个主机名,使用 该 IP;不要再解析。”
- 发起请求。
只会进行一次解析,库随后再也不会执行 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 资产创建的自托管实例,请立即更新。