写点什么

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:162306

评论

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

NGINX 与当下爆火的 ChatGPT 聊天,回答质量参差不齐

NGINX开源社区

nginx ChatGPT

百度工程师移动开发避坑指南——内存泄漏篇

百度Geek说

ios android 开发 企业号 5 月 PK 榜

【经验总结】你想知道的BGA焊接问题都在这里

华秋PCB

工具 电路 PCB PCB设计 焊接

小程序6大开发框架对比分析

Onegun

小程序 小程序框架 小程序容器

软件测试 | 接口测试

测吧(北京)科技有限公司

测试

TDenigne 签约路特斯科技,助力高性能跑车领域数据架构升级

TDengine

时序数据库 #TDengine

MobTech ShareSDK|助力预热618

MobTech袤博科技

年营收将破千亿?运营商云的底气在哪里?

ToB行业头条

数据挖掘实践(金融风控):金融风控之贷款违约预测挑战赛(下篇)[xgboots/lightgbm/Catboost等模型]--模型融合:stacking、blending

汀丶人工智能

人工智能 数据挖掘 机器学习 深度学习 数学建模

数据在 Mocaverse 项目启动过程中是如何发挥作用的

Footprint Analytics

区块链游戏 NFT 链游 Mocaverse

2023年汽车软件行业趋势分析:安全性是汽车软件开发的重大挑战2023年汽车软件开发

龙智—DevSecOps解决方案

汽车软件安全 电动汽车软件 汽车软件开发

“敏捷教练必修课程”7月22-23日 ·A-CSM认证在线周末班【提前报名特惠】CST导师亲授

ShineScrum

Scrum 敏捷 敏捷精髓 敏捷实践 A-CSM

软件测试/测试开发丨Python控制流-判断&循环

测试人

Python 软件测试 自动化测试 测试开发

CST为什么要关闭 GPU 卡的 ECC 模式而开启 TCC 模式?操作使用【详解】

思茂信息

cst cst使用教程 电磁仿真 cst电磁仿真 cst仿真软件

面面俱到!一份囊括35+核心知识点的Java架构师面试文档

小小怪下士

Java 程序员 java面试 Java八股文

软件测试 | 开发接口

测吧(北京)科技有限公司

测试

FP&A整合,全面预算管理的制胜法宝

智达方通

全面预算管理 财务规划和分析 FP&A

龙智即将亮相2023上海国际嵌入式展,为嵌入式开发提供全球领先解决方案

龙智—DevSecOps解决方案

嵌入式软件 嵌入式设计 嵌入式开发

4大特性看Huawei Cloud EulerOS为开发者带来平滑迁移体验

华为云开发者联盟

云计算 后端 华为云 华为云开发者联盟 企业号 5 月 PK 榜

Neuron 提供免费无限时试用:完整体验数十种工业协议连接

EMQ映云科技

工业物联网 网关软件 工业协议

融云 WICC 2023 定档!「出海嘉年华」穂城来袭!

融云 RongCloud

通信 社交 融云 出海 wicc

ControlNet: 控制扩散模型的魔法

Zilliz

AIGC Towhee Stable Diffustion controlnet

“全球金牌课程”6月17-18日 · CSM认证在线周末班【提前报名特惠】CST导师亲授

ShineScrum

Scrum 敏捷

【论文分享|SIGMOD'22】WeTune 自动发现和验证重写规则

Databend

重磅!用友荣登全球5强

用友BIP

数据挖掘实践(金融风控):金融风控之贷款违约预测挑战赛(上篇)[xgboots/lightgbm/Catboost等模型]--模型融合:stacking、blending

汀丶人工智能

数据挖掘 机器学习 深度学习 数据建模

打造面向未来的开发者服务新范式,龙蜥社区开发者服务平台 devFree MeetUp 硬核启动!欢迎报名

OpenAnolis小助手

Meetup 龙蜥社区 基础设施SIG devFree 开发者服务平台

Apache Kylin 5.0.0-alpha 正式发布,能力全方位提升!

Kyligence

开源 Apache Kylin

一文看懂OpenStack SR-IOV

统信软件

领先芯片供应商u-blox通过Perforce Helix Core加强协作,实现基于组件的开发

龙智—DevSecOps解决方案

组件化 芯片设计

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