一句话版本
现代软件 90% 以上的代码来自第三方依赖。只要其中任何一个包被投毒,所有使用它的项目都会中招。常见手法有:冒名(注册一个和知名包只差一个字母的名字)、接管(盗取或骗取维护者账号)、长期潜伏(以贡献者身份花几年时间获得信任,再植入后门)、依赖混淆(让构建工具从公共仓库下载一个和内部包同名的恶意包)。 2024 年的 xz-utils 后门是最令人警醒的例子:攻击者用了约两年时间成为维护者,差一点就让后门进入主流 Linux 发行版。
全文分三层:
- 通俗理解:餐厅的食材来自几百家供应商
- 实际用处:event-stream、ua-parser-js、xz-utils、依赖混淆
- 原理与工程要点:安装脚本、锁文件与哈希、私有镜像、SBOM
第一层:通俗理解
1.1 几百家供应商
一家餐厅的菜,食材来自几百家供应商,供应商的供应商又有几百家。只要其中任何一家往原料里掺了东西,所有用这批原料的餐厅都会出事。
一个普通的 JavaScript 项目,直接依赖几十个包,加上间接依赖可能有上千个。你真正读过代码的,可能一个都没有。
1.2 四种投毒方式
| 方式 | 比喻 |
|---|---|
| 冒名(typosquatting) | 开一家叫"可口可楽"的店 |
| 账号接管 | 偷走正规供应商的公章 |
| 长期潜伏 | 应聘进正规供应商,干了两年当上主管,再往原料里掺东西 |
| 依赖混淆 | 你公司内部有个叫"秘制酱料"的配方,有人在外面注册了同名商品,采购员买错了 |
第二层:实际用处
| 事件 | 方式 | 经过 |
|---|---|---|
| event-stream(2018) | 维护权转手 | 原维护者无暇维护,把这个每周下载数百万次的 npm 包交给了一个主动提出帮忙的陌生人。新维护者加入了一个依赖,其中的恶意代码专门针对一个比特币钱包应用,窃取用户的钱包 |
| ua-parser-js(2021) | 账号被盗 | 维护者的 npm 账号被盗,攻击者发布了带有挖矿程序和密码窃取程序的版本 |
| 依赖混淆(2021) | 同名公共包 | 安全研究者发现,很多公司的构建系统在找内部包时,会优先从公共仓库下载版本号更高的同名包。他借此(以无害的方式)进入了数十家大公司的内部网络,获得了大额漏洞赏金 |
| xz-utils(2024,CVE-2024-3094) | 长期潜伏 | 一个化名开发者花了约两年时间为这个基础压缩库贡献代码、获得维护者信任,最终在发布包(而不是源码仓库)中植入了一个针对 SSH 的复杂后门。微软工程师 Andres Freund 因为注意到 SSH 登录慢了约半秒、CPU 占用异常,追查后发现了它。此时后门已进入几个发行版的测试版本 |
xz 事件暴露了开源基础设施的一个结构性问题:大量关键组件由一两个疲惫的志愿者维护。
第三层:原理与工程要点
3.1 安装就是执行
很多包管理器允许包在安装时运行脚本(如 npm 的 postinstall、Python 的 setup.py)。这意味着:npm install 某个包 就等于在你的电脑上以你的身份执行了它的作者写的代码。不需要你真的调用这个包。
对策:在 CI 和不需要的场合禁用安装脚本(如 npm ci --ignore-scripts),只对确实需要的包单独允许。
3.2 锁文件与哈希
| 措施 | 作用 |
|---|---|
锁文件(package-lock.json、poetry.lock、Cargo.lock) |
固定每个依赖(包括间接依赖)的确切版本 |
| 完整性哈希 | 锁文件里记录每个包的哈希,下载后校验,防止仓库里的同版本文件被替换 |
CI 用 ci 而不是 install |
严格按锁文件安装,锁文件和清单不一致就报错 |
| 更新要审查 | 依赖更新走人工审查的 PR,看变更内容,而不是自动合并 |
3.3 防依赖混淆
- 内部包使用组织作用域(如
@公司名/包名),并在公共仓库里注册这个作用域; - 配置包管理器:内部作用域只从私有仓库拉取;
- 使用私有镜像(代理),所有公共包也经过它,可以集中审查、缓存和阻断。
3.4 识别可疑的包
| 信号 | 说明 |
|---|---|
| 名字和知名包极其相似 | typosquatting |
| 刚发布不久、下载量低,却被你的依赖引入 | 可能是新植入的恶意依赖 |
| 维护者最近变更 | event-stream 的教训 |
| 新版本突然加入安装脚本、网络请求、混淆代码 | 高危 |
| 发布的包内容和源码仓库不一致 | xz 的后门就只存在于发布包里 |
有专门的工具和服务(如 Socket、OpenSSF Scorecard、各平台的依赖扫描)可以自动检查这些信号。
3.5 SBOM 和长期措施
- SBOM(软件物料清单):记录产品里包含的所有组件和版本。出现像 xz 这样的事件时,能在几分钟内回答"我们受影响吗?";
- 构建来源证明(如 SLSA、Sigstore):证明一个发布包确实由某个源码提交、在某个构建系统里构建出来,防止"发布包和源码不一致";
- 支持关键的开源维护者:从根本上减少"疲惫的独立维护者"这一风险点。
总结
| 层次 | 要点 |
|---|---|
| 风险 | 上千个依赖,任何一个被投毒都会中招 |
| 手法 | 冒名、账号接管、长期潜伏、依赖混淆 |
| 关键事实 | 安装就是执行 |
| 对策 | 锁文件 + 哈希;禁用安装脚本;作用域 + 私有镜像;审查更新;SBOM 与来源证明 |