作者 | Darcy
译者 | 核子可乐
策划 | 丁晓昀
最近,npm 前工程经理 Darcy 在一份报告中指出,npm 注册没有根据相应 包的内容验证清单信息。 说,这会导致双重事实来源,攻击者可以利用它来隐藏脚本或依赖项。
这一点影响很大。例如,npm 上有个包可能会显示它没有依赖项,而实际上它有。同样,它显示的包名或版本可能与 .json 中的不同,而这可能会导致缓存中毒。更糟糕的是,它可以隐藏它将在安装期间运行脚本的事实。
在接受 InfoQ 采访时, 安全研究员 Ax 强调,这种不一致不一定是恶意的,可能是源于合法的克隆或分叉,或者是由于开发人员在更新包时没有清理过时的元数据。他还提出了一点小小的异议:
相信 .json 并不一定比相信包的 npmjs 页面更好——两者都不是完全可靠的。
根据 的说法,要解决这个问题需要借助安全工具进行更深入的分析,例如,对恶意文件或受到攻击的文件进行基于散列的分析,即高级二进制指纹。
另一个有用的建议来自 J. M. Rossy 的推特,他建议默认关闭脚本。
如果你对这个清单之惑感兴趣,请阅读 的原文,其中有许多其他的见解。
以下为原文翻译。
简单自我介绍,2019 年 7 月至 2022 年 12 月期间,我负责 npm CLI 团队的工程管理。2020 年我参与了 收购 npm 项目.。2022 年 12 月,我因各种原因离开了 。
如今,各类新兴供应链攻击可谓层出不穷,而本文要向大家分享的则是其中一例——我个人称之为“ 混淆”( )。
故事背景
在 Node 生态系统发展到如今全球用户达数千万、创建超过 310 万个软件包、月下载量高达 2080 亿次的规模之前,当初该项目的贡献者数量曾非常有限。当然,社区越小,大家就越感觉安心,毕竟没有哪个黑客团队会找这么“瘦”的目标下手。但随着时间推移,npm 注册表被逐步开发出来,人们可以免费贡献并检查其中的开源代码,语料库的组织政策和实践也迎来同步发展。
从诞生之初,npm 项目就非常信任注册表的客户端与服务器端。现在回想起来,这种高度依赖客户端来处理数据验证的作法真的很有问题。但也正是凭借这项策略,是让 工具生态得以快速成长并在数据形态中有所体现。
发生甚么事了?
npm 公共注册表不会使用包 中的内容来验证 信息npm,反而是依赖 npm 兼容的客户端进行解释和强制验证 / 一致性。事实上,在研究这个问题时,我发现服务器似乎从未承担过验证任务。
如今, 允许用户通过 PUT 请求将软件包发布至相应的包 URI,例如:
。
该端点会接收一条请求 body,内容如下所示(请注意:在经历近 15 年的发展之前,如今的 npm 及其他注册表 API 仍然严重缺乏记录信息):
{
_id: ,
name: ,
'dist-tags': { ... },
versions: {
'': {
_id: '@`,
name: '',
version: '',
dist: {
integrity: '',
shasum: '',
tarball: ''
}
...
}
},
_attachments: {
0: {
content_type: 'application/octet-stream',
data: '',
length: ''
}
}
}
目前的问题是, 元数据(也就是「」数据)是独立于存放有软件包 .json 的 而独立提交的。这两部分信息之间从未进行过相互验证,而且我们往往搞不清依赖项、脚本、许可证等数据的“权威事实来源”究竟是谁。据我所知, 才是唯一拥有签名,且有着可离线存储及验证的完整性值的工件。从这些角度看,它应该才是正确的来源;但令人意外的是,.json 当中的 name & 字段实际上很可能与 中的字段不同,因为二者间不会进行相互验证。
示例
1.在 上生成身份验证令牌 (例如://new - 选择 "" 以方便测试)
2.启动一个新项目 (例如:mkdir test && cd test/ && npm init -y)
3.安装 库(例如:npm ssri npm--fetch)
4.创建一个子目录,作为“真实”的软件包及内容(例如 mkdir pkg && cd pkg/ && npm init -y)
5.修改该包的内容……
6.在项目根目录中创建一个 .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 = ''
const 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': '',
method: 'PUT',
body: manifest,
})
})()
7.可随意修改其中 键(例如,我在这里去掉了 & );
8.运行程序(例如:node .js);
9.导航至
/ &
/v/?= 以查看差异。
以上示例中的软件包是用不同 发布的,其各有对应的 .json,请参考:
Bug, bug, 到处是 bug
如果大家想用更简单的办法重现这种不一致性,现在也可以使用 npm CLI。一旦在项目中发现 .gyp 文件,它就会在 npm 发布期间改变 内容。这种行为似乎在我加入 npm 团队之前(即 6.x 或更早版本)就已经存在于客户端内,而且已经给众多用户惹出了不少麻烦。
1.npm init -y
2.touch .gyp
3.npm
4.可以看到, "node-gyp " . 条目已被自动添加至 当中,但却未被添加至 的 .json 当中。例如:
这种不一致现象在 node- 中经常出现:
相关影响
这个 bug 可能会以多种方式影响消费者 / 最终用户:
1.缓存中毒(即保存的包可能与注册表 /URI 中的名称 + 版本规格不匹配;
2.安装未知 / 未列出的依赖项(欺骗安全 / 审计工具);
3.安装未知 / 未列出的脚本(欺骗安全 / 审计工具);
4.引发潜在降级攻击(保存到项目中的版本规格,为不符合要求 / 易受攻击的包版本)。
已知受到影响的
第三方组织 / 实体:
更新:前文提到, 易受到 混淆问题的影响。自 2022 年 9 月 5 日起, 方面已开始使用 内的 .json 文件作为事实来源,且要求显示包的准确信息(例如依赖项、许可证、脚本等)。截至本文发布时,--pkg 的软件包页面错误地引用了过时的数据,但 团队很快解决了这个问题。这里要称赞一声, 可能是首个正确处理此问题的项目团队。
此问题还会以下面介绍的几种方式,影响到所有已知的主要 包管理器。jFrog 的 等第三方注册表实现似乎也继承了该 API 的设计 / 问题,因此使用这些私有注册表实例的所有客户端也会出现相同的问题 / 不一致。
注意,各类包管理器和工具对应不同的应用场景。它们要么使用 / 引用软件包的注册表 ,要么使用 / 引用 的 .json(主要是为了通过缓存机制提高安装性能)。
这里需要强调的是,生态系统目前仍普遍存在错误假设,即 的内容始终与 的 .json 内容一致(这主要是因为注册表 API 说明文档过少,且 多次提到注册表会将 .json 的内容存储为元数据——但却没有强调其实是由客户端负责确保一致性)。
npm@6
执行 / 中不存在的脚本
重现步骤:
1.安装一个格式经过篡改的依赖项: npx npm@6 --pkg@2.1.13
2.See that are being even none are in the & the has not the as (ie. 可以看到,虽然在 中并不存在 / 注册表尚未将包注册为具有安装脚本,但生命周期脚本仍在执行(即 为 /false)
参考
代码 / 包请参考
3./--pkg 当中的 .json 反映 条目。
安装 / 中不存在的依赖项
由于包 会被缓存在全局存储当中,所以如果 --- 配置与 --no--lock 共同使用,则下一次在系统中对该包运行 时,隐藏在 中的依赖项也会被安装。
重现步骤:
1.安装 npx npm@6 --pkg@2.1.13
2.再次运行安装… npx npm@6 --- --no--lock
npm@9
安装 / 中不存在的依赖项
与 npm@6, 类似,npm@9 在使用 -- 配置时也会直接安装经过缓存的 .json 当中引用的依赖项。
注意:其中似乎存在争用条件,即 -- 可能会 / 可能不会被从缓存内提取npm,因此重现结果并不稳定。
重现步骤:
1.安装格式经过篡改的依赖项以将其缓存;
2.在安装时使用 -- 配置并 / 或关闭可用网络(例如:npm -- --no--lock)
3.可以看到, 中并未引用的依赖项也会被安装。
yarn@1
执行 / 中不存在的安装脚本
与 npm@6 & npm@9 类似,yarn@1 会 中引用、但 并未引用的脚本,反之亦然。
使用 中的 字段——暴露潜在降级攻击向量
现在大家已经了解, 的内容定义可以与 有所不同;在这种情况下,yarn@1 顺理成章地在升级 / 降级之后,再把错误版本保存回当前项目的 .json 当中(可能令用户在后续安装中遭受降级攻击)。
pnpm@7
执行 / 中不存在的安装脚本
重现步骤:
与之前几个案例类似,pnpm 会运行 中存在、但 并未引用的脚本,反之亦然。
CWE 分类 / 细分
此漏洞可能涉及多种 CWE 分类。至少如果我们把此问题视为“特例”,则以上情况应该被归纳为“服务端安全的客户端实施”(即 CWE-602——但我严重怀疑这种判断并不适用。我在下文中会具体分析各种问题及其相应 CWE 分类,且分别提供参考代码)。
1.CWE-602: 服务端安全的客户端实施
2.CWE-94: 代码生成控制不当(「代码注入」)
3.CWE-295: 证书生成不当
4.CWE-325: 缺少加密步骤
5.CWE-656: 依赖构建于封闭的安全性
为此做了哪些努力?
据我所知, 大概在 2022 年 11 月 4 日左右发现了这个问题;经过独立研究之后,我认为这个问题的潜在影响 / 风险要比最初的判断大得多,因此于 3 月 9 日提交了一份包含个人发现的 报告。3 月 21 日, 关闭了该工单,表示他们正在“内部”处理这个问题。据我了解,之后 没有取得任何 重大进展,也没有公开发布这个问题。相反,他们在过去半年间逐渐放弃了 npm 的产品地位,且拒绝更新或提供关于补救措施的相关说明。
可行的解决方案
正陷入不可逆转的困境。事实上, 就是在这样的状态下运行了十余年,意味着目前的安全状况已经被深深嵌入代码当中,再难实现广泛修复。如前所述,npm CLI 本身也依赖于这种设计,而且目前还可能存在其他非恶意用途。
用户能做点什么?
与认识的任何使用 npm 注册表 数据的已知工具作者 / 维护者联系,确保他们知情并想办法在适当时转而使用包内容作为元数据(即除了 name & 之外的所有内容)。另外,请从现在起严格执行 / 验证注册表代理的一致性。
原文链接:
限时特惠:本站持续每日更新海量各大内部创业课程,一年会员仅需要98元,全站资源免费下载
点击查看详情
站长微信:Jiucxh