hello,我是海海
这一期是关于
npm
漏洞的一篇译作。阅读时间15分钟欢迎转载,请注明原文和作者
有任何疑惑的地方,欢迎后台留言
声明:本文观点除了笔者明确添加的表示自己的部分,其他均为原作者观点,与本人无关。
原文标题:The massive bug at the heart of the npm ecosystem
原文作者:Darcy Clarke
文章分类:安全
,npm
,registry
,一致性校验
核心观点:第三方库的mainfest
和实际上传到包发布平台的tarball
不会被校验,导致用户安装依赖时产生安全问题
本文涉及到的相关术语:
CWE category
:由开源社区维护的通用缺陷列表。有超过600个常见缺陷,包括缓冲溢出,路径索引遍历树错误,竞争状态条件,跨站脚本,硬编码密码以及不安全随机数等registry
:本文指npm registry
,是一个公开的在线数据库,存储了开发者创建与共享的各种代码包。比如:registry.npmjs。与包发布平台有所不同,前者是一个数据库,后者是检索网站。tarball
:一个计算机术语,通常用来指通过tar
命令创建的归档文件/压缩文件。mainfest
:在软件和编程中,通常指一个元数据文件,它包含了关于包和软件项目的重要信息。在本文中,与package.json
有所不同:作为包的开发者,mainfest
是上传文件时http
请求的信息;作为包的使用者,mainfest
是package-lock.json
中对应包的相关信息(版本、tarball源文件的指向等)。- 降级攻击:一种攻击方式,攻击者会试图使系统使用旧的、可能存在安全漏洞的软件版本,而不是最新的,已修复的最新版本。在本文中,特指恶意包强迫用户安装低版本的依赖包,间接达到攻击的目的。
本期大纲:
npm
历史回顾- 性能与安全的权衡——
npm
有什么漏洞? - 通过设计一个demo,说明一下
- 这个漏洞会如何影响用户?
- 这个漏洞影响了哪些平台?
- 这个漏洞包含了哪些
CWE category
? - 针对这个漏洞,
GitHub
平台做了什么? - 针对这个漏洞,有什么解决方案?
npm
历史回顾
nodejs
生态发展至今,已有数以千万计的开发者创建了超过310万个软件包。每月下载次数超过2080亿次。
最开始,在npm
社区,参与开源软件贡献的人非常少,这使得社区之间的信任度较高,因为每个人都可以贡献并检查代码。(可以理解为,参与的开发者都是出于善意的,并且他们的技术水平高)。
但是,随着时间的推移和生态系统的发展,社区的规则也在不断变化。
起初,npm
项目信任(依赖)客户端校验,即在用户安装依赖时在本地校验依赖的包信息、完整性和子依赖树等。
虽说如此严重依赖客户端的做法是充满问题的,但是这种策略也促进了JavaScript
工具生态系统的繁荣。
性能与安全的权衡——npm
有什么漏洞?
(以下为笔者添加)这里需要介绍下我们平时开发时的流程,以便我们更好理解
npm
的漏洞:开发时,当我们第一次安装依赖,此时项目没有
package-lock.json
,npm
会检索并安装包,同时生成对应的mainfest
——package-lock.json
。因此我们知道,package-lock.json
除了锁定版本之外,还能提高安装效率,因为无需重新构建依赖树关系。似乎,
package-lock.json
没有什么不好。但是,当其他用户通过package-lock.json
安装依赖时,可能会出现package-lock.json
和安装包信息不匹配的情况。详情见下文及图1-1。
图1-1
registry
数据库并不会使用tarball
的内容来验证mainfest
,而是依赖客户端进行一致性验证。事实上,当我编写这篇文章时,registry
服务器还未解决这个问题。
如今,registry.npmjs.com
允许用户通过PUT
请求向相应的包URI
发布包,请求body
如下所示。
{
// mainfest
_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>'
}
}
}
不过,尽管已经过去了大约十五年,npm
注册表的API
文档仍然非常不完善(包括API
的URL
、请求方法、请求参数、响应格式等)。
备注:为什么
API
文档不完善会造成这个漏洞?原因:
node
生态系统是一个假设为前提的,这个假设就是mainfest
始终包含tarball
的package.json
,但是如果API
文档完善,就不会搞这个假设。
另外一个问题是,包的mainfest
(如上述代码所示)是独立于包的package.json
(附加在tarball
)提交的。这两个文件的信息从未相互验证过,因此无法规范dependencies
,scripts
,license
等字段。我认为,tarball
是唯一获得签名并具有可以离线存储和验证的完整性的证据。
备注:下文中作者通过设计了一个demo
,强调了npm
包不会进行验证tarbar
和mainfest
的观点。
通过设计一个demo,说明一下
- 1、在
npmjs.com
生成身份验证令牌auth token
- 2、创建一个新项目:
mkdir test && cd test/ && npm init -y
- 3、下载3个包:
npm install ssri libnpmpack npm-registry-fetch
。作用分别是:防篡改、打包项目到npm、从registry数据库获取包信息、下载包 - 4、项目下创建一个子目录pkg,它将充当我们测试包一致性的目标
mkdir pkg && cd pkg/ && npm init -y
- 5、修改
pkg
的内容 - 6、在项目根目录下创建一个
publish.js
文件,代码如下 - 7、根据需要,修改
mainfest
信息,比如删除scripts
和dependencies
- 8、运行程序:
node publish.js
- 9、导航到
registry.npmjs.com/pkg
和npmjs.com/package/pkg/v/version
以查看差异,即对比数据库和检索网站间的差异,如下图1-2所示
;(async () => {
// libs
const ssri = require('ssri')
const pack = require('libnpmpack')
const fetch = require('npm-registry-fetch')
// 步骤一:打包文件、生成一致性hash
// pack tarball & generate ingetrity
const tarball = await pack('./pkg/')
const integrity = ssri.fromData(tarball, {
algorithms: [...new Set(['sha1', 'sha512'])],
})
// 步骤二:创建mainfest信息
// 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,
},
},
}
// 步骤三:将包发布到registry数据库
// publish via PUT
fetch(name, {
'//registry.npmjs.org/:_authToken': '<auth token>',
method: 'PUT',
body: manifest,
})
})()
图1-2
补充说明:node-canvas
就是其中一个“受害者”
这个漏洞会如何影响用户?
作者按照危害等级列举了以下影响:
- (低)下载时,用户下载的包与
registry
对应的包信息不匹配 - (中)下载时:用户会下载不在
mainfest
中出现的依赖,比如病毒包、可以绕过安全检测和其他的恶意包等 - (高)安装后,用户执行相关
scripts
/ 不存在的scripts
可能会触发意料之外的事情。 - (非常高)安装后,上述的恶意包会使用降级攻击。
这个漏洞影响了哪些平台?
已知的受影响的第三方组织和实体/CDN:
- Synk:开源安全平台,它可以检测和修复开源依赖中的安全漏洞
- CNPMJS/Chinese:
npm
中国镜像 - Cloudflare:由
Cloudflare
提供的npm
镜像服务 - Skypack:直接在浏览器中使用
npm
包,无需打包或构建步骤 - UNPKG:快速、全球可用的
npm
包内容的CDN
- JSPM:可以在浏览器和
Node.js
中使用 - Yarn:快速、可靠、安全的依赖管理工具,由Facebook, Google, Exponent 和 Tilde 联合推出
备注:作者的特别表扬
由于Socket Security
容易受到该问题的影响(笔者对这个地方不甚了解,知道的同学可以在评论区发表你的见解),Socket
团队自2022年9月5日起,明确使用tarball
内的package.json
文件作为标准,并要求必须显示包的准确信息(依赖项、许可证、脚本)。
Socket
团队应该是在该领域第一个正确处理此问题的团队。(优秀!)
通过socket.dev
网站,我们可以看到不同包的安全程度。如下图1-3所示。
图1-3
这个漏洞包含了哪些CWE category
?
- CWE-602:由客户端实施服务端安全
- CWE-94:代码生成控制不当
- CWE-295:证书生成不当
- CWE-325:缺少加密步骤
- CWE-656:基于缺乏信息构建的安全性
针对这个漏洞,GitHub
平台做了什么?
据我所知,Github首次意识到这个问题是2022年11月4日左右;经过独立研究后,我相信这个问题的潜在影响/风险实际上比最初我们认为的要更大。
我于2023年3月9日提交了一份包含我发现的HackerOne报告,Github关闭了它并表示他们于3月21日在内部处理了这个问题。
据我所知,他们没有取得任何重大进展,也没有公开这个问题。相反,实际上他们在过去6个月内减少了在npm产品上的投入,并拒绝跟进或提供任何补救工作的见解。
(笔者观点)原文作者在这里吐槽了Github对该漏洞的做法
针对这个漏洞,有什么解决方案?
Github陷入困境是可以被理解的。事实上,npmjs.com
已经以这种方式运行了十多年了。npm-cli本身依赖这种机制,改变这种机制可能会影响用户的使用习惯。
具体的解决方案:
- 确认
registry
受影响的部分,即通过人工或自动化的方式排查问题 - 如果只有少数项目因为包收到了影响,可以手动修复package.json
- 排查和日常开发可以是并行,不必担心你的开发因为这个问题被阻塞
- 从根源上,需要尽快记录npm-registry的API信息,包括请求和相应的格式
感谢你的耐心阅读,如果觉得好的话,可以给我点个赞吗
创作不易,感谢你的支持!
这是我的微信公众号,欢迎和我一起玩前端
本文使用 markdown.com.cn 排版