摘要
本篇为以下原文的翻译。
2019年7月至2022年12月期间,npm CLI团队的员工工程经理。他是2020年GitHub收购npm inc.时的一员。由于各种原因,他于12月离开了GitHub。
-
npm 包的清单是独立于其 tarball 发布的,清单从未根据 tarball 的内容进行完全验证。
-
生态系统广泛假设清单和 tarball 的内容是一致的
-
使用公共注册表的任何工具或见解都容易被利用/可能不准确
-
不良行为者可以将恶意软件和脚本隐藏在直接或传递依赖项中而未被检测到
就新颖的供应链攻击而言,这是一个大问题,从现在开始,我将其称为**“明显的混乱”**。
事件背景
在节点生态系统成为今天的样子之前 – 又名。全球数以千万计的开发人员创建了超过310 万个软件包,每月下载次数为2080 亿次 – 为您信任使用和下载的软件库做出贡献的人数非常少。社区越小,您就越信任,即使 npm 注册表正在开发中,大多数方面都是开源的,可以免费贡献和检查代码。但是,随着时间的推移,随着生态系统的发展,使用语料库的组织的政策和实践也在不断发展。
从一开始,npm 项目就非常信任注册表的客户端和服务器端。现在回想起来,很明显,如此严重依赖客户端来处理数据验证的做法是充满问题的,但该策略也允许 JavaScript 工具生态系统有机增长并参与数据的形成。
怎么了?
npm 公共注册表不会使用包 tarball 的内容来验证清单信息,而是依赖 npm 兼容的客户端来解释和强制验证/一致性。事实上,当我研究这个问题时,服务器似乎从未做过此验证(因此您可能想将其称为“功能”)。
现在,用户可以通过向相应的包 URI(例如 )发出请求registry.npmjs.com
来发布包。这个端点接受一个看起来像这样的请求(注意:近十五年后,这个和所有其他注册表 API 仍然没有记录):PUT``https://registry.npmjs.com/-/<package-name>``body
{
_id: <pkg>,
name: <pkg>,
'dist-tags': { ... },
versions: {
'<version>': {
_id: '<pkg>@<version>`,
name: '<pkg>',
version: '<version>',
dist: {
integrity: '<tarball-sha512-hash>',
shasum: '<tarball-sha1-hash>',
tarball: ''
}
...
}
},
_attachments: {
0: {
content_type: 'application/octet-stream',
data: '<tarball-base64-string>',
length: '<tarball-length>'
}
}
}
目前的问题是,version
元数据(又名“清单”数据)的提交独立于包含包的package.json
. 这两条信息从未相互验证过,并且让人质疑哪一条应该是诸如dependencies
、等数据scripts
的“真实的规范来源”。license
据我所知,tarball 是唯一获得签名并具有可以离线存储和验证的完整性值的工件(使其有可能成为正确的源;然而,非常令人惊讶的是name
,实际上可能与清单中的不同,因为它们从未经过验证)。version``package.json
例子
- 在 npmjs.com 上生成身份验证令牌(例如
https://www.npmjs.com/settings/<your-username>/tokens/new
– 为了方便起见,选择“自动化”) - 开始一个新项目(例如
mkdir test && cd test/ && npm init -y
) - 安装辅助库(例如
npm install ssri libnpmpack npm-registry-fetch
) - 创建一个子目录,它将充当“真正的”包和内容(例如
mkdir pkg && cd pkg/ && npm init -y
) - 修改该包的内容…
- 在项目根目录中创建一个
publish.js
文件,内容如下:
;(async () => {
// libs
const ssri = require('ssri')
const pack = require('libnpmpack')
const fetch = require('npm-registry-fetch')
// pack tarball & generate ingetrity
const tarball = await pack('./pkg/')
const integrity = ssri.fromData(tarball, {
algorithms: [...new Set(['sha1', 'sha512'])],
})
// craft manifest
const name = '<pkg name>'
const version = '<pkg version>'
const manifest = {
_id: name,
name: name,
'dist-tags': {
latest: version,
},
versions: {
[version]: {
_id: `${name}@${version}`,
name,
version,
dist: {
integrity: integrity.sha512[0].toString(),
shasum: integrity.sha1[0].hexDigest(),
tarball: '',
},
scripts: {},
dependencies: {},
},
},
_attachments: {
0: {
content_type: 'application/octet-stream',
data: tarball.toString('base64'),
length: tarball.length,
},
},
}
// publish via PUT
fetch(name, {
'//registry.npmjs.org/:_authToken': '<auth token>',
method: 'PUT',
body: manifest,
})
})()
manifest
根据需要修改键(例如,我在上面删除了scripts
& )dependencies
- 运行程序(例如
node publish.js
) - 导航至
https://registry.npmjs.com/<pkg>/
&https://www.npmjs.com/package/<pkg>/v/<version>?activeTab=explore
查看差异
在上面的示例中,包是使用不同的清单发布的,然后是相应的package.json
(参考www.npmjs.com/darcyclarke… /](registry.npmjs.com/darcyclarke…
error
如果您想要一种更简单的方法来重现这种不一致,您现在可以使用 CLI ,因为它实际上会在看到项目中的文件npm
时改变清单。这种行为似乎在我加入团队之前(即或更早)就已经存在于客户端中,并且是消费者产生许多错误/困惑的原因。npm publish``binding.gyp``<6.x
npm init -y
touch binding.gyp
npm publish
- 查看条目
"node-gyp rebuild"
scripts.install
已自动添加到清单中,但不是实际的 tarballpackage.json
(例如registry.npmjs.com/darcyclarke… json](unpkg.com/darcyclarke…
影响
该错误实际上通过多种方式影响消费者/最终用户:
- 缓存中毒(即保存的包可能与注册表/URI 中的名称+版本规范不匹配)
- 安装未知/未列出的依赖项(欺骗安全/审核工具)
- 执行未知/未列出的脚本(欺骗安全/审核工具)
- 潜在的降级攻击(其中保存到项目中的版本规范是针对未指定的、易受攻击的包版本)
受影响的已知第三方组织/实体
- 斯尼克:security.snyk.io/package/npm…
- CNPMJS/中文镜像:npmmirror.com/package/dar…
- Cloudflare 镜像:registry.npmjs.cf/darcycle-ma…
- Skypack:cdn.skypack.dev/-/darcyclar…
- UNPKG:unpkg.com/darcyclarke…
- JSPM:ga.jspm.io/npm: darcyc…
- 纱线:yarnpkg.com/package/dar…
此问题还会以下面详细介绍的各种方式影响所有已知的主要 JavaScript 包管理器。jFrog 的 Artifacory 等第三方注册表实现似乎也复制了此 API 设计/问题,这意味着这些私有注册表实例的所有客户端都会注意到相同的问题/不一致。
值得注意的是,各种包管理器和工具具有不同的场景,在这些场景中,它们将使用/引用包的注册表清单或 tarball package.json
(几乎总是作为缓存和提高安装性能的机制)。
这里要指出的关键点是,生态系统目前处于错误的假设之下,即清单始终包含 tarball 的内容package.json
(这在很大程度上是因为严重缺乏注册表 API 文档以及 docs.npmjs 中的各种引用) .com 的事实是,注册表将内容存储package.json
为元数据 – 并且它没有在任何地方提到客户端负责确保一致性)。
npm@6
执行清单中不存在的安装脚本,反之亦然
重现步骤:
- 安装格式错误的依赖项:
npx npm@6 install darcyclarke-manifest-pkg@2.1.13
- 看到生命周期脚本正在执行,即使清单中不存在并且注册表尚未将包注册为具有安装脚本(即 /
hasInstallScript
)undefined
(false
参考registry.npmjs.org/darcyclarke… – 代码/包参考。https: //github.com/npm/minify-registry-metadata/blob/main/lib/index.js) - in
package.json
反映node_modules/darcyclarke-manifest-pkg
tarball 条目
安装清单中不存在的依赖项,反之亦然
由于包 tarball 会缓存在全局存储中,因此如果--prefer-offline
将配置与 一起使用--no-package-lock
,则下次install
在系统中运行同一包时,可能会安装隐藏在 tarball 中的依赖项。
重现步骤:
- 安装
npx npm@6 install darcyclarke-manifest-pkg@2.1.13
- 在某处再次运行安装…
npx npm@6 install --prefer-offline --no-package-lock
npm@9
安装清单中不存在的依赖项,反之亦然
与 类似npm@6
,在使用配置时npm@9
会愉快地安装包的缓存 tarball 中引用的依赖项。package.json``--offline
注意:似乎存在竞争条件,
--offline
可能会或可能不会从缓存中提取,从而导致间歇性结果
重现步骤:
- 安装格式错误的依赖项以便将其缓存
--offline
通过配置和/或关闭网络可用性(例如npm install --offline --no-package-lock
)来运行安装- 查看清单中未引用的依赖项将被安装
yarn@1
执行清单中不存在的安装脚本,反之亦然
与npm@6
&一样npm@9
,yarn@1
将运行 tarball 内但未在清单中引用的脚本,反之亦然。
使用version
tarball 中发现的内容 – 暴露潜在的降级攻击向量
众所周知,tarball 可以有version
与清单不同的定义;在这种情况下,yarn@1
将愉快地升级/降级并保存回使用项目的package.json
错误版本(可能使消费者在后续安装中遭受降级攻击)
pnpm@7
执行清单中不存在的安装脚本,反之亦然
重现步骤:
与所有其他脚本一样,pnpm
将运行 tarball 内但未在清单中引用的脚本,反之亦然。
CWE 分类/细分
此漏洞可能有多种 CWE 分类。至少,如果这个问题可能被视为一个“功能”,那么我们在这里看到的必须被视为“服务器端安全的客户端执行”(即。) – 但我怀疑这是适用的最小范围CWE-602
。我在下面分解了各种问题及其相应的 CWE 分类(每种情况都提供了代码参考)。
- CWE-602:服务器端安全的客户端实施
- 长期以来,我们一直严重依赖客户端(又名 CLI
npm
)来完成本应在服务器端完成的工作;这是一个完美的例子 - 代码参考 github.com/npm/cli/blo…
- 长期以来,我们一直严重依赖客户端(又名 CLI
- CWE-94:代码生成控制不当(“代码注入”)
- 这与任何/所有消费者相关(包括包管理器,例如
npm
);如下所述,因此它们都有各种问题
- 这与任何/所有消费者相关(包括包管理器,例如
- CWE-295:证书生成不当
- tarball 已签名并赋予完整性值,即使其内容(包括
name
、version
、dependencies
、license
等scripts
)与其关联的注册表索引不同
- tarball 已签名并赋予完整性值,即使其内容(包括
- CWE-325:缺少加密步骤
- 清单数据未签名,因此无法离线缓存或验证
- 缺少与 tarball
package.json
和包清单重叠的键的数据子集的哈希值/验证
- CWE-656:通过默默无闻来依赖安全性
- 由于完全缺乏有关注册表 API 的文档,这个问题不容易辨别
GitHub 对此做了什么?
据我所知,GitHub 首次意识到这个问题是在 2022 年 11 月 4 日左右;经过独立研究后,我相信这个问题的潜在影响/风险实际上比最初理解的要大得多,我于 3 月 9 日提交了一份包含我的发现的 HackerOne 报告。GitHub 关闭了该票证并表示他们正在“内部”处理这个问题3月21日。据我所知,他们没有取得任何重大进展,也没有公开这个问题 – 相反,他们实际上在过去 6 个月里放弃了在 npm 作为产品的地位,并拒绝跟进或提供任何补救措施的见解工作。
解决方案会是什么样子?
GitHub 正陷入不可逆转的困境。事实上,npmjs.com
这种方式已经运行了十多年,这意味着当前的状态几乎已经被编成法典,并且很可能以一种独特的方式破坏某人。如前所述,npm
CLI 本身依赖于这种行为,而且目前这种行为还可能存在其他非恶意用途。
- 应该做什么…
- 应该进行进一步调查以确定注册表中受影响条目的范围,这将有助于确定滥用情况
- 如果差异的数量很小(考虑到飞行中的清单突变似乎有多普遍,这是值得怀疑的),那么我想根据 tarball 的差异重新生成清单是有意义的
package.json
- 开始强制/验证清单中的特权/已知密钥可以与任何研究/发现异步发生
- npm 公共注册表 API 及其各自的请求/响应对象需要尽快记录下来
你能做什么?
联系您知道依赖于 npm 注册表清单数据的任何已知工具作者/维护者,并确保他们在适当的时候开始使用包的内容作为元数据(即 除了 name
&以外的所有内容version
)。开始使用严格执行/验证一致性的注册表代理。