【FCon上海】与行业领袖共话AI大模型、数字化风控等前沿技术。 了解详情
写点什么

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

作者 | Darcy Clarke

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

    阅读完需:约 20 分钟

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

AI 大模型超全落地场景&金融应用实践,8 月 16 - 19 日 FCon x AICon 大会联诀来袭、干货翻倍!

最近,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:162024

评论

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

二分实现及工程使用—Kafka

工程师日月

算法 java 编程 5月月更

Flutter 开源状态管理插件一览

岛上码农

flutter ios 安卓 移动端开发 5月月更

linux之grep使用技巧

入门小站

Linux

微信业务架构&学生管理系统架构

intelamd

吐血整理!42个人工智能机器学习数据集推荐!

澳鹏Appen

人工智能 机器学习 大数据 计算机视觉 数据集

云钉一体:EventBridge 联合钉钉连接器打通云钉生态

阿里巴巴云原生

阿里云 云原生 事件总线 EventBridge

毫秒级返回数据,58同城 DBA 团队选择 TDengine 解决传感器数据处理难题

TDengine

数据库 tdengine

Redis「7」实现分布式锁

Samson

redis 学习笔记 5月月更

Cocos creatorの摇杆操控运动

空城机

Cocos 5月月更

【LeetCode】链表的中间结点Java题解

Albert

LeetCode 5月月更

开启Tomcat管理注主页功能

jiangxl

tomcat

云原生赋能开发测试

百度Geek说

元原生

火山引擎大规模机器学习平台架构设计与应用实践

火山引擎开发者社区

人工智能 机器学习

代码语言的魅力

百度Geek说

Java 8 开始新增的 Optional 类 - Optional 对象中的返回

HoneyMoose

架构实战营 模块一作业

Gor

Seata x 2022 开源之夏 ,一起来做开源达人!

阿里巴巴云原生

阿里云 云原生 seata 开源之夏

“学生管理系统”毕设架构设计

Pengfei

架构设计原则

在线HTML转CSV工具

入门小站

工具

[Day42]-[回溯]-组合

方勇(gopher)

LeetCode 数据结构和算法 回溯算法

企评家,企业成长性评价为创业板企业投融资决策提供信息支持

企评家

⭐万字长篇超详细的图解Tomcat中间件方方面面储备知识⭐

jiangxl

tomcat Java web

在线文本列表差集计算工具

入门小站

工具

通过JConsoler监控Tomcat的JVM内存

jiangxl

企评家 |江西长运股份有限公司成长性评价简介

企评家

Bootstrap 和 WordPress 的区别

海拥(haiyong.site)

bootstrap Wordpress 博客部署 WordPress 5月月更

数据库连接池 -Druid 源码学习(八)

wjchenge

Druid 数据库连接池

Tomcat安全优化

jiangxl

tomcat Java web

Linux环境下部署Jpress大型博客网站

jiangxl

企评家 |上海家化联合股份有限公司成长性评价简介

企评家

JAVA程序对应不同的部署环境针对配置文件如何管理

jiangxl

Java tomcat

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