npm前员工自曝生态内部存在严重bug (npm的工作内容)

npm前员工自曝生态内部存在严重bug (npm的工作内容)

在接受 InfoQ 采访时,Sonatype 安全研究员 Ax Sharma 强调,这种不一致不一定是恶意的,可能是源于合法的克隆或分叉,或者是由于开发人员在更新包时没有清理过时的元数据。他还提出了一点小小的异议:

根据 Sharma 的说法,要解决这个问题需要借助安全工具进行更深入的分析,例如,对恶意文件或受到攻击的文件进行基于散列的分析,即高级二进制指纹。

另一个有用的建议来自 J. M. Rossy 的推特,他建议默认关闭脚本。

如果你对这个清单之惑感兴趣,请阅读 Clarke 的原文,其中有许多其他的见解。

如今,各类新兴供应链攻击可谓层出不穷,而本文要向大家分享的则是其中一例——我个人称之为“manifest 混淆”(manifest confusion)。

在 Node 生态系统发展到如今全球用户达数千万、创建超过 310 万个软件包、月下载量高达 2080 亿次的规模之前,当初该项目的贡献者数量曾非常有限。当然,社区越小,大家就越感觉安心,毕竟没有哪个黑客团队会找这么“瘦”的目标下手。但随着时间推移,npm 注册表被逐步开发出来,人们可以免费贡献并检查其中的开源代码,语料库的组织政策和实践也迎来同步发展。

从诞生之初,npm 项目就非常信任注册表的客户端与服务器端。现在回想起来,这种高度依赖客户端来处理数据验证的作法真的很有问题。但也正是凭借这项策略,是让 JavaScript 工具生态得以快速成长并在数据形态中有所体现。

npm 公共注册表不会使用包 tarball 中的内容来验证 manifest 信息,反而是依赖 npm 兼容的客户端进行解释和强制验证/一致性。事实上,在研究这个问题时,我发现服务器似乎从未承担过验证任务。

如今,registry.npmjs.com 允许用户通过 PUT 请求将软件包发布至相应的包 URI,例如:

该端点会接收一条请求 body,内容如下所示(请注意:在经历近 15 年的发展之前,如今的 npm 及其他注册表 API 仍然严重缺乏记录信息):

'dist-tags': { ... },versions: {version: '<version>',integrity: '<tarball-sha512-hash>',shasum: '<tarball-sha1-hash>',tarball: ''_attachments: {content_type: 'application/octet-stream',
复制代码

目前的问题是,version 元数据(也就是「manifest」数据)是独立于存放有软件包 package.json 的 tarball 而独立提交的。这两部分信息之间从未进行过相互验证,而且我们往往搞不清依赖项、脚本、许可证等数据的“权威事实来源”究竟是谁。据我所知,tarball 才是唯一拥有签名,且有着可离线存储及验证的完整性值的工件。从这些角度看,它应该才是正确的来源;但令人意外的是,package.json 当中的 name&version字段实际上很可能与 manifest 中的字段不同,因为二者间不会进行相互验证。

;(async () => {const ssri = require('ssri')const pack = require('libnpmpack')const fetch = require('npm-registry-fetch')// pack tarball & generate ingetrityconst tarball = await pack('./pkg/')const integrity = ssri.fromData(tarball, {algorithms: [...new Set(['sha1', 'sha512'])],// craft manifestconst name = '<pkg name>'const version = '<pkg version>'const manifest = {name: name,'dist-tags': {latest: version,versions: {[version]: {_id: `${name}@${version}`,integrity: integrity.sha512[0].toString(),shasum: integrity.sha1[0].hexDigest(),tarball: '',scripts: {},dependencies: {},_attachments: {content_type: 'application/octet-stream',data: tarball.toString('base64'),length: tarball.length,// publish via PUTfetch(name, {'//registry.npmjs.org/:_authToken': '<auth token>',method: 'PUT',body: manifest,
复制代码

以上示例中的软件包是用不同 manifest 发布的,其各有对应的 package.json,请参考:

如果大家想用更简单的办法重现这种不一致性,现在也可以使用 npm CLI。一旦在项目中发现 binding.gyp 文件,它就会在 npm 发布期间改变 manifest 内容。这种行为似乎在我加入 npm 团队之前(即 6.x 或更早版本)就已经存在于客户端内,而且已经给众多用户惹出了不少麻烦。

这种不一致现象在node-canvas 中经常出现:

这个 bug 可能会以多种方式影响消费者/最终用户:

已知受到影响的第三方组织/实体:

此问题还会以下面介绍的几种方式,影响到所有已知的主要 JavaScript 包管理器。jFrog 的 Artifacory 等第三方注册表实现似乎也继承了该 API 的设计/问题,因此使用这些私有注册表实例的所有客户端也会出现相同的问题/不一致。

注意,各类包管理器和工具对应不同的应用场景。它们要么使用/引用软件包的注册表 manifest,要么使用/引用 tarball 的 package.json(主要是为了通过缓存机制提高安装性能)。

这里需要强调的是,生态系统目前仍普遍存在错误假设,即 manifest 的内容始终与 tarball 的 package.json 内容一致(这主要是因为注册表 API 说明文档过少,且 docs.npmjs.com 多次提到注册表会将 package.json 的内容存储为元数据——但却没有强调其实是由客户端负责确保一致性)。

src="https://static001.geekbang.org/infoq/71/71cbb2e4e858254b0c5c93e13a633440.png"/>

安装 manifest/tarball 中不存在的依赖项

由于包 tarball 会被缓存在全局存储当中,所以如果--prefer-offline 配置与--no-package-lock 共同使用,则下一次在系统中对该包运行 install 时,隐藏在 tarball 中的依赖项也会被安装。

安装 manifest/tarball 中不存在的依赖项

与 npm@6,类似,npm@9 在使用--offline 配置时也会直接安装经过缓存的 tarball package.json 当中引用的依赖项。

执行 manifest/tarball 中不存在的安装脚本

与 npm@6 & npm@9 类似,yarn@1 会 tarball 中引用、但 manifest 并未引用的脚本,反之亦然。

使用 tarball 中的 version 字段——暴露潜在降级攻击向量

现在大家已经了解,tarball 的内容定义可以与 manifest 有所不同;在这种情况下,yarn@1 顺理成章地在升级/降级之后,再把错误版本保存回当前项目的 package.json 当中(可能令用户在后续安装中遭受降级攻击)。

执行 manifest/tarball 中不存在的安装脚本

与之前几个案例类似,pnpm 会运行 tarball 中存在、但 manifest 并未引用的脚本,反之亦然。

此漏洞可能涉及多种 CWE 分类。至少如果我们把此问题视为“特例”,则以上情况应该被归纳为“服务端安全的客户端实施”(即 CWE-602——但我严重怀疑这种判断并不适用。我在下文中会具体分析各种问题及其相应 CWE 分类,且分别提供参考代码)。

据我所知,GitHub 大概在 2022 年 11 月 4 日左右发现了这个问题;经过独立研究之后,我认为这个问题的潜在影响/风险要比最初的判断大得多,因此于 3 月 9 日提交了一份包含个人发现的 HackerOne 报告。3 月 21 日,GitHub 关闭了该工单,表示他们正在“内部”处理这个问题。据我了解,之后 GitHub 没有取得任何重大进展,也没有公开发布这个问题。相反,他们在过去半年间逐渐放弃了 npm 的产品地位,且拒绝更新或提供关于补救措施的相关说明。

GitHub 正陷入不可逆转的困境。事实上,npmjs.com 就是在这样的状态下运行了十余年,意味着目前的安全状况已经被深深嵌入代码当中,再难实现广泛修复。如前所述,npmCLI 本身也依赖于这种设计,而且目前还可能存在其他非恶意用途。

与认识的任何使用 npm 注册表 manifest 数据的已知工具作者/维护者联系,确保他们知情并想办法在适当时转而使用包内容作为元数据(即除了 name&version 之外的所有内容)。另外,请从现在起严格执行/验证注册表代理的一致性。

top="10106">相关阅读:

前端开发:node.js 的 node 包管理器npm安装以及...

NPM实用命令与快捷方式

前端包管理工具npmyarn cnpm npx

Npm,Inc. 发布NpmPro,面向独立 JavaScript 开发人员

声明:本文来自用户分享和网络收集,仅供学习与参考,测试请备份。