攻击原理

很多组织会同时使用:

  • 公共包仓库(如 npm、PyPI、Maven Central);
  • 私有包仓库(存放公司内部的库)。

问题:当包管理器解析一个依赖时,如果内外仓库中存在同名包,它会选哪个?

很多包管理器默认选择版本号更高的那个——而不区分来源。

攻击方式:

  1. 攻击者通过某种途径获知某公司内部使用的一个私有包名(如 acme-internal-utils);

    • 泄露途径:GitHub 上公开的代码、package.json、错误日志、招聘信息、Docker 镜像、JS bundle 中的路径……
  2. 攻击者在公共仓库上发布一个同名包,版本号高于内部包的版本;
  3. 当该公司的构建系统拉取依赖时,包管理器选择了公共仓库的恶意高版本包;
  4. 恶意代码在构建环境(或生产环境)中执行。

这个攻击的巧妙之处:不需要攻破任何系统,只需要"提供一个更好的报价"。

影响

  • 在 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 后门事件的警示)。

一句话

你的依赖,就是你的攻击面。 管理依赖来源,和保护自己的代码同样重要。

说明:本文为供应链安全的知识整理。