Cargo deny 与依赖治理:阻止有漏洞的 crate 进入你的项目

Cargo deny 与依赖治理:阻止有漏洞的 crate 进入你的项目 Cargo deny 与依赖治理阻止有漏洞的 crate 进入你的项目更可怕的是我问了自己三个问题这些 crate 有已知漏洞吗它们的开源许可证我能商用吗有没有重复编译不同版本的同一个库比如syn 1.x和syn 2.x同时存在——我一个都答不上来。今天这篇文章就是我用cargo-deny来解决这三个问题的一手记录。一、cargo-deny 是什么为什么需要它cargo-deny是一个 Cargo 子命令专门做依赖审计Dependency Audit。它检查三件事安全漏洞Advisories依赖的 crate 是否在 RustSec 数据库中登记了已知漏洞许可证合规Licenses依赖的许可证是否与你的项目兼容依赖源头Sources依赖是否来自受信任的源比如 crates.io还有一个很实用的功能Ban 列表——你可以禁止特定 crate或特定版本进入项目。# 安装 cargo-deny只需要做一次 cargo install cargo-deny # 在项目根目录生成默认配置文件 cargo deny init # 运行完整的审计检查 cargo deny check第一次跑完cargo deny check后的输出大概是这样error[A001]: net2 0.2.39 has been yanked! ┌─ /home/user/project/Cargo.lock:42:1 │ 42 │ net2 0.2.39 registryhttps://github.com/rust-lang/crates.io-index │ ------------------------------------------------------------- yanked crate detected │ net2 v0.2.39 └── mio v0.6.23 └── tokio-reactor v0.1.12 └── ... error[L001]: failed to get license requirements ┌─ ring 0.16.20 │ ring v0.16.20 ├── LICENSE: OpenSSL └── warning[B004]: found 2 duplicate entries for crate syn ┌─ /home/user/project/Cargo.lock:85:1 │ 85 │ syn 1.0.109 registryhttps://github.com/rust-lang/crates.io-index │ ------------------------------------------------------------- syn v1.0.109 │ 87 │ syn 2.0.87 registryhttps://github.com/rust-lang/crates.io-index │ --------------------------------------------------------- syn v2.0.87初次看到这个输出时我心里只有一个想法幸好跑了。然后开始逐项修。二、配置 deny.toml定制你的依赖策略cargo deny init生成的配置文件是deny.toml这是整个依赖治理的核心。下面是我在 AI CLI 项目中实际使用的配置逐段解读# deny.toml # 这是我 AI CLI 项目的依赖审计配置 # ---------- 1. 安全漏洞 ---------- [advisories] # 忽略社区尚未处理的漏洞谨慎使用 # 只有确认过不影响自己项目才能加进来 ignore [ # 例子RUSTSEC-2020-0071time crate 的段错误问题 # 我确认了我的项目不使用 time crate 的受影响 API # RUSTSEC-2020-0071, ] # 漏洞数据库的 URL默认是 GitHub 上的 RustSec 仓库 db-url https://github.com/rustsec/advisory-db # 拉取间隔多久检查一次是否有新的漏洞报告 db-path ~/.cargo/advisory-db # 如果 CI 中因为网络问题拉不到数据库宽限期允许跳过 # 生产构建时不建议跳过设置成 [] 就是不留情面 yanked deny unmaintained warn # 无人维护的 crate给警告 unsound deny # API 不安全但未修复拒绝 notice warn # 一般性公告提醒但允许 # ---------- 2. 许可证合规 ---------- [licenses] # 我的 AI CLI 是 MIT 许可证只允许以下兼容的许可证 allow [ MIT, Apache-2.0, Apache-2.0 WITH LLVM-exception, BSD-3-Clause, Unicode-DFS-2016, ISC, MPL-2.0, # Mozilla 公共许可证 BSL-1.0, # Boost 软件许可证 CC0-1.0, # 公共领域 ] # 如果有依赖的许可证不兼容CI 直接挂掉 # 但允许一些例外手动确认过合规的 exceptions [ # 某些 crate 许可证文件名称不规范手动指定 # { name ring, allow [OpenSSL] }, ] # 未知许可证的处理有些 crate 没声明许可证 unknown deny # 对商业项目这是必须的 copyleft deny # GPL/AGPL 等传染性许可证不能用 # ---------- 3. 禁止列表 ---------- [bans] # 禁止引入某些 crate deny [ # 阻止 unicode-* 系列老 crate 的意外引入 # 因为 Rust 标准库已经内置了 unicode 支持 { name openssl-sys, wrappers [] }, # 我只用 rustls { name native-tls, wrappers [] }, # 同上强制 rustls ] # 允许重复依赖不触发 warning 的例外 # 有时候同一个库的多个版本是被迫的间接依赖冲突 skip [ { name windows-sys, version 0.45 }, { name windows-sys, version 0.48 }, { name windows-sys, version 0.52 }, # ↑ Windows API 的版本碎片化是 ecosystem 的问题 # 我们作为应用层只能接受但不加 skip 会一直报警 ] # 精简依赖树某些 crate 有更好更小的替代品 skip-tree [ # 如果项目中已经没有使用 time crate但依然出现在依赖树里 # 这通常是某个间接依赖的残留需要排查 ] # ---------- 4. 依赖源限制 ---------- [sources] # 只允许来自官方 crates.io 的依赖 # 如果你的项目还会从私有 registry 拉需要在这里配置 unknown-registry deny unknown-git deny # 不允许直接依赖 Git 仓库 # 如果需要内网私有 registry在这里配置 # 但我的 AI CLI 只用公共 crates allow-registry [https://github.com/rust-lang/crates.io-index]配置完跑一遍之后所有不合规的 crafe 都会被打回——在 CI 里加了这步之后我再也没遇到过不小心引了个 GPL 的库的尴尬情况。flowchart TD A[推送代码 / 合并 PR] -- B[CI: cargo deny check] B -- C{检查1: 安全漏洞} C --|通过| D{检查2: 许可证兼容} C --|失败| C1[CI 失败br/→ 通知开发者修复] D --|通过| E{检查3: Ban 列表} D --|失败| D1[CI 失败br/→ 替换为兼容许可证的替代库] E --|通过| F{检查4: 依赖源} E --|失败| E1[CI 失败br/→ 去除被禁用的 crate] F --|通过| G[✅ 构建继续] F --|失败| F1[CI 失败br/→ 确认依赖来源] style C1 fill:#ffcdd2 style D1 fill:#ffcdd2 style E1 fill:#ffcdd2 style F1 fill:#ffcdd2 style G fill:#c8e6c9三、修复实战常见问题怎么解跑完cargo deny check之后你一定会遇到一堆报错。下面是我踩过的几个典型坑及修复方法# 1. 重复依赖Duplicate crate # 问题syn v1.0 和 syn v2.0 同时出现 # 原因某个间接依赖还在用 syn v1 # 解决方法 # ① 运行 cargo tree -i syn 查看是谁引用了旧版 # ② 如果那个 crate 有更新版本支持 syn v2升级它 # ③ 如果是社区的废弃 crate考虑替换或 fork # 可以在 [bans] 中配置 [bans] multiple-versions deny highlight all # 在输出中高亮显示所有重复的 crate # 2. yanked crate # 问题某个版本被作者或社区撤回了 # 解决直接升级或降级没有商量余地 # 配置中已设 yanked denyCI 会自动拦住 # 3. 许可证不兼容 # 问题某个传递依赖用了 GPL-3.0 # 解决 # ① cargo tree -i crate-name 看是谁带进来的 # ② 找替代品功能相同但许可证更宽松的 crate # ③ 如果是必需的且确认合规在 licenses.exceptions 中显式声明来看一个实际修复例子。我的项目引用了reqwestHTTP 客户端它默认依赖native-tls基于 OpenSSL// 修复前Cargo.toml // reqwest 默认带 native-tls引入了一堆 OpenSSL 依赖 // [dependencies] // reqwest { version 0.12, features [json] } // ↓↓↓ 换成 rustls ↓↓↓# 修复后Cargo.toml # 把 native-tls 换成纯 Rust 实现的 rustls [dependencies] reqwest { version 0.12, default-features false, features [ json, rustls-tls, # ← 用 rustls 代替 native-tls http2, ]} # 依赖树对比运行 cargo tree 可以看到 # 修复前reqwest → hyper-tls → native-tls → openssl → openssl-sys # 修复后reqwest → hyper-rustls → rustls → ring纯 Rust不仅解决了 OpenSSL 许可证问题还减少了编译时间openssl-sys需要 C 编译器二进制体积也小了。四、把 cargo-deny 嵌入 CI/CD依赖审计最大的价值在于自动化。手动跑一次只能解决当下的问题但新人加了个依赖、或者老依赖被发现新漏洞你不会知道。在 GitHub Actions 中配置# .github/workflows/audit.yml name: 依赖审计 on: push: branches: [main] pull_request: branches: [main] # 每天定时跑一次漏洞数据库在更新 schedule: - cron: 0 8 * * * # 每天早上 8 点 jobs: audit: name: Security Audit License Check runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: 安装 Rust 工具链 uses: dtolnay/rust-toolchainstable - name: 安装 cargo-deny run: cargo install cargo-deny --locked - name: 运行依赖审计 run: cargo deny check advisories bans licenses sources # 四个子命令分别检查 # advisories → 安全漏洞 # bans → 禁止列表 重复依赖 # licenses → 许可证合规 # sources → 依赖源gantt title 依赖治理的持续监控节奏 dateFormat HH:mm axisFormat %H:%M section 每次 PR cargo deny check :active, pr1, 00:00, 2min section 每日定时 拉取 RustSec 数据库 :daily1, 08:00, 30s 全量审计扫描 :daily2, after daily1, 2min 发送告警如有漏洞:daily3, after daily2, 10s section 每月 依赖更新评审 :monthly1, 00:00, 30min 淘汰过时依赖 :monthly2, after monthly1, 20mincargo-deny 的定时任务还需要注意schedule总是跑在默认分支如果 main 分支和开发分支的deny.toml不一致定时审计报的漏洞可能跟线上实际环境对不上。建议在 CI 脚本里显式 checkout 到稳定分支。还有一个现实问题cargo-deny报的 RUSTSEC 漏洞有些是理论上的生产环境不一定能被利用。对于无法立即升级的依赖可以先用deny.toml的skip字段放行但必须加注释写明原因和计划修复时间。裸的 skip 没有注释一星期后没人记得为什么绕过。五、总结这篇文章我从287 个依赖我一个都不知道的恐慌出发介绍了cargo-deny的配置和使用方法以及如何把它嵌入 CI/CD 流程中。Rust 生态的 crate 数量庞大、质量参差不齐依赖治理不是一次性的工作而是一个持续的习惯。我的建议是项目一开始就配置 cargo-deny不要等项目 500 个依赖了才想起来审计那时候改一个传递依赖可能需要重构半个项目CI 里跑不是本地跑手动跑没有持续性下一周可能就忘不要跳过 yanked deny看似方便实则埋雷依赖少就是最好的安全策略定期运行cargo tree看看哪些依赖可以去掉作为自学者依赖治理这个词听起来很高级工程师但实操起来其实就是 cargo deny check 看报错 改 Cargo.toml 三步走。不要被术语吓到动手跑一次就明白了。如果你的项目从来没跑过依赖审计今天就可以试试cargo deny init cargo deny check惊喜或者惊吓在等着你。我们下篇见