写点什么

npm 前员工自曝生态内部存在严重 bug | 附避坑指南

作者 | Darcy Clarke

  • 2023-07-24
    北京
  • 本文字数:5984 字

    阅读完需:约 20 分钟

npm前员工自曝生态内部存在严重bug | 附避坑指南

最近,npm 前工程经理 Darcy Clarke 在一份报告中指出,npm 注册没有根据相应 tarball 包的内容验证清单信息。Clarke 说,这会导致双重事实来源,攻击者可以利用它来隐藏脚本或依赖项。

 

这一点影响很大。例如,npm 上有个包可能会显示它没有依赖项,而实际上它有。同样,它显示的包名或版本可能与 package.json 中的不同,而这可能会导致缓存中毒。更糟糕的是,它可以隐藏它将在安装期间运行脚本的事实。

 

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

 

相信 package.json 并不一定比相信包的 npmjs 页面更好——两者都不是完全可靠的。

 

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

 

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

 

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

 

以下为原文翻译。

 

简单自我介绍,2019 年 7 月至 2022 年 12 月期间,我负责 npm CLI 团队的工程管理。2020 年我参与了 GitHub 收购 npm 项目.。2022 年 12 月,我因各种原因离开了 GitHub。

 

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

 

故事背景

 

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

 

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

 

发生甚么事了?

 

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

 

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

 

https://registry.npmjs.com/-/<package-name>。

 

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

 

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

示例


  1. 在 npmjs.com 上生成身份验证令牌(例如: https://www.npmjs.com/settings/<your-username>/tokens/new - 选择 "Automation" 以方便测试)

  2. 启动一个新项目 (例如: mkdir test && cd test/ && npm init -y)

  3. 安装 helper 库(例如:npm install ssri libnpmpack npm-registry-fetch)

  4. 创建一个子目录,作为“真实”的软件包及内容(例如 mkdir pkg && cd pkg/ && npm init -y)

  5. 修改该包的内容……

  6. 在项目根目录中创建一个 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, })})()
复制代码

 

  1. 可随意修改其中 manifest 键(例如,我在这里去掉了 scripts & dependencies );

  2. 运行程序(例如: node publish.js);

  3. 导航至 https://registry.npmjs.com/<pkg>/ & https://www.npmjs.com/package/<pkg>/v/<version>?activeTab=explore 以查看差异。

 


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

 https://www.npmjs.com/darcyclarke-manifest-pkg 

 https://registry.npmjs.com/darcyclarke-manifest-pkg/

 Bug, bug, 到处是 bug

 

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

 

  1. npm init -y

  2. touch binding.gyp

  3. npm publish

  4. 可以看到, "node-gyp rebuild" scripts.install 条目已被自动添加至 manifest 当中,但却未被添加至 tarball 的 package.json 当中。例如:

 

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

 

相关影响

 

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

  1. 缓存中毒(即保存的包可能与注册表/URI 中的名称+版本规格不匹配;

  2. 安装未知/未列出的依赖项(欺骗安全/审计工具);

  3. 安装未知/未列出的脚本(欺骗安全/审计工具);

  4. 引发潜在降级攻击(保存到项目中的版本规格,为不符合要求/易受攻击的包版本)。

 

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

 

 

更新:前文提到,Socket Security 易受到 manifest 混淆问题的影响。自 2022 年 9 月 5 日起,Socket 方面已开始使用 tarball 内的 package.json 文件作为事实来源,且要求显示包的准确信息(例如依赖项、许可证、脚本等)。截至本文发布时,darcyclarke0-manifest-pkg 的软件包页面错误地引用了过时的数据,但 Socket 团队很快解决了这个问题。这里要称赞一声,Socket 可能是首个正确处理此问题的项目团队。

 

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

 

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

 

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

 

npm@6

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


重现步骤:

  1. 安装一个格式经过篡改的依赖项: npx npm@6 install darcyclarke-manifest-pkg@2.1.13

  2. See that lifecycle scripts are being executed even though none are present in the manifest & the registry has not registered the package as having install script (ie.可以看到,虽然在 manifest 中并不存在/注册表尚未将包注册为具有安装脚本,但生命周期脚本仍在执行(即 hasInstallScript 为 undefined/false) 参考:

 https://registry.npmjs.org/darcyclarke-manifest-pkg/2.1.13 


代码/包请参考:

https://github.com/npm/minify-registry-metadata/blob/main/lib/index.js

node_modules/darcyclarke-manifest-pkg 当中的 package.json 反映 tarball 条目。



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

 

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

重现步骤:

  1. 安装 npx npm@6 install darcyclarke-manifest-pkg@2.1.13

  2. 再次运行安装… npx npm@6 install --prefer-offline --no-package-lock



npm@9

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


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

 

注意:其中似乎存在争用条件,即--offline 可能会/可能不会被从缓存内提取,因此重现结果并不稳定。

 

重现步骤:

  1. 安装格式经过篡改的依赖项以将其缓存;

  2. 在安装时使用--offline 配置并/或关闭可用网络(例如: npm install --offline --no-package-lock)

  3. 可以看到,manifest 中并未引用的依赖项也会被安装。

 

yarn@1

 

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

 

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



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


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



pnpm@7

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


重现步骤:

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



CWE 分类/细分

 

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


GitHub 为此做了哪些努力?

 

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

可行的解决方案

 

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


  • 该做点什么……

  • 应开展进一步调查,确定注册表内受影响条目的具体范围,这将有助于确定滥用情况。

  • 如果差异量不太大(但考虑到当前 manifest 变体的可观规模,这种可能性恐怕很低),那么也许可以根据 tarball 的 package.json 重新生成包含差异的 manifest。

  • 从现在起,根据研究/发现对 manifest 中高权限/已知密钥强制执行/验证。

  • 尽快将 npm 公共注册表 API 及其相应的请求/响应对象记录下来。

用户能做点什么?

 

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

原文链接:

https://blog.vlt.sh/blog/the-massive-hole-in-the-npm-ecosystem

相关阅读:

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

NPM 实用命令与快捷方式

前端包管理工具 npm yarn cnpm npx

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


2023-07-24 11:162189

评论

发布
暂无评论
发现更多内容

Java数据处理的常用技术,springboot源码解读与原理分析

Java 程序员 后端

Java百度云,springboot实例教程,面试大厂应该注意哪些问题

Java 程序员 后端

阿里云重磅发布业务中台产品 BizWorks,中台发展进入下一个阶段

阿里巴巴云原生

阿里云 云原生 业务中台 云栖大会 BizWorks

Java教程视频百度网盘,小甲鱼数据结构百度云,腾讯Java面试题

Java 程序员 后端

Java爬虫爬取视频,尚硅谷笔试答案,最全面试考点与面试技巧

Java 程序员 后端

Java技术基础知识总结,菜鸟教程mysql,Java重要知识点

Java 程序员 后端

云栖发布|企业级互联网架构全新升级 ,助力数字创新

阿里巴巴云原生

阿里云 云原生 产品升级 云栖大会

Java的Io模型你了解多少?尚硅谷大厂学院课,Java开发面试笔试题大汇总

Java 程序员 后端

Java程序员全套,百度三面牛客网猿生活,疯狂膜拜

Java 程序员 后端

Java技术成长,kafka学习教程,Java开发者面试如何系统复习

Java 程序员 后端

Java排序算法面试,黑马java项目一,springboot实战项目源码

Java 程序员 后端

Java春招实习面试经验汇总,图灵学院诸葛,Java微服务架构视频下载

Java 程序员 后端

Java月薪过万要掌握的技能,javajdk下载教程,高级Java工程师面试问题

Java 程序员 后端

Java架构师进阶之路,马士兵的java教程,大厂Java面试总结+详细解答

Java 程序员 后端

Java程序员面试笔记,极客时间vue开发实战,Java进阶教程视频

Java 程序员 后端

Java教程百度云最新版,java写脚本教程视频,程序员必须要了解的知识点

Java 程序员 后端

Java教程百度云最新版,极客时间vip年卡,Java开发者面试如何系统复习

Java 程序员 后端

Java数据结构面试题,java架构师指南下载百度,Java工程师面试题及答案

Java 程序员 后端

Java知识体系!极客学院黑马程序员,BIO和NIO有啥区别

Java 程序员 后端

Java百度云资源,java基础案例教程黑马程序员在线阅读,美团Java面试流程

Java 程序员 后端

Java程序员最新职业规划,尚学堂高琪300集,初级Java工程师面试题

Java 程序员 后端

Java研发岗面试复盘总,4面技术5面HR附加笔试面

Java 程序员 后端

Java程序员如何有效提升学习效率,如何化身BAT面试收割机

Java 程序员 后端

Java程序员必会!开课吧java高级架构师课程,Java开发大厂面试经验

Java 程序员 后端

Java程序员面试中最容易答错的8道面试题,tomcat面试题及答案

Java 程序员 后端

Java日常开发的12个坑,你踩过几个,一招让你拿下seata分布式事务框架

Java 程序员 后端

Java架构师必备技能,java程序设计实用教程第五版答案,掌握这个提升路径

Java 程序员 后端

Java框架,黑马java视频教程,面试资料分享

Java 程序员 后端

Java百度云教程,深入java虚拟机百度云,附详细答案

Java 程序员 后端

Java知识体系!java黑马视频和达内,链表反转的两种实现方法

Java 程序员 后端

Java研发岗必问30+道高级面试题,腾讯,字节等大厂面试真题汇总

Java 程序员 后端

npm前员工自曝生态内部存在严重bug | 附避坑指南_工程化_InfoQ精选文章