攻击原理
很多组织会同时使用:
- 公共包仓库(如 npm、PyPI、Maven Central);
- 私有包仓库(存放公司内部的库)。
问题:当包管理器解析一个依赖时,如果内外仓库中存在同名包,它会选哪个?
很多包管理器默认选择版本号更高的那个——而不区分来源。
攻击方式:
攻击者通过某种途径获知某公司内部使用的一个私有包名(如
acme-internal-utils);- 泄露途径:GitHub 上公开的代码、package.json、错误日志、招聘信息、Docker 镜像、JS bundle 中的路径……
- 攻击者在公共仓库上发布一个同名包,版本号高于内部包的版本;
- 当该公司的构建系统拉取依赖时,包管理器选择了公共仓库的恶意高版本包;
- 恶意代码在构建环境(或生产环境)中执行。
这个攻击的巧妙之处:不需要攻破任何系统,只需要"提供一个更好的报价"。
影响
- 在 CI/CD 环境中执行任意代码 → 窃取凭据、植入后门;
- 如果恶意包进入生产部署 → 直接影响线上系统。
2021 年,安全研究者 Alex Birsan 公开演示了这类攻击,成功影响的组织包括多家大型科技公司——证明了攻击的可行性。
防御
一、明确指定包的来源(核心)
不同的包管理器有不同的机制:
npm:使用 scope(@acme/utils),并在 .npmrc 中把 @acme scope 绑定到私有仓库:
@acme:registry=https://npm.internal.acme.com/pip:使用 --index-url 指定单一索引,或用 --extra-index-url 时配合版本锁定和哈希校验。
关键:不要把私有包和公共包混在同一个"扁平"的解析空间中。
Maven/Gradle:明确声明仓库,并为内部包使用专属的 groupId。
Go:使用 GOPRIVATE / GONOSUMDB 环境变量,指定私有模块路径。
二、版本锁定 + 完整性校验
- 锁文件(package-lock.json、poetry.lock、go.sum)锁定确切版本;
- 哈希校验:锁文件中包含包的哈希,包管理器验证下载的内容与哈希一致。
这能防止"即使解析到了错误的包,内容也与预期不符"——虽然会导致构建失败(也是一种保护)。
三、命名空间预留
在公共仓库上抢先注册你的内部包名(即使是空包),防止被攻击者占用。
这是一个低成本的缓解措施——但要注意,维护这些占位包也有成本,且不是所有仓库都允许长期占位。
四、使用代理仓库 / 单一入口
在企业内部搭建包仓库代理(如 Nexus、Artifactory),它:
- 统一管理公共包和私有包的访问;
- 可以配置规则:私有命名空间只从内部仓库获取,不向公共仓库查询;
- 缓存公共包,减少对外部的依赖。
这是最可靠的企业级方案。
五、监控
- 监控构建日志中包的来源;
- 使用依赖审计工具(如
npm audit、pip-audit、Snyk、Dependabot); - 生成并维护 SBOM(前文提到过),以便快速响应。
相关的供应链攻击
依赖混淆只是供应链攻击的一种。其它包括:
一、账号劫持(Account Takeover)
攻击者获取维护者的 npm/PyPI 账号(通过钓鱼、凭据泄露),发布带有恶意代码的新版本。
由于很多项目依赖版本范围(如 ^1.2.0),下次构建就会自动拉取恶意版本。
防御:锁定版本 + 哈希校验 + 及时更新(矛盾在于:及时更新可能拉到恶意版本)。
二、typosquatting(抢注相似名)
发布与流行包名称相近的包(如把 lodash 拼成 lodahs、requests 拼成 request),等待有人打错字。
防御:仔细检查包名;使用锁文件。
三、恶意贡献者
通过长期参与获得信任,成为维护者后植入后门(前文提到过 xz 后门事件的警示)。
一句话
你的依赖,就是你的攻击面。 管理依赖来源,和保护自己的代码同样重要。
说明:本文为供应链安全的知识整理。