事情是这样的,由于我们项目使用了这个 Yapi 项目,前几天收到了安全组的预警通知,说 Yapi 被爆出了安全漏洞,新注册的用户,可以在你的服务器执行任意代码,删除任意东西,让我紧急修改!!
心想这么大个开源项目(21.7k),居然有这么严重的安全漏洞,不应该呀。
相信很多小伙伴都用过这款开源软件来用作接口管理工具,但是为了防止有一些小伙伴不知道这个库是干嘛的,我就大概介绍一下。
YApi (https://github.com/YMFE/yapi)是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
它不仅支持 docker 部署,而且还有很多插件可以使用,例如自动化测试插件、自动生成代码等等功能。
嗯,接着说,我于是就拿着安全组的复现路径来复现 Yapi 的漏洞。
首先我在我的 /Users/qiufeng/my/yapi
的目录下创建了一个 1.js
。
然后打开 Yapi 一个项目 —— 点击设置 ——全局mock脚本,然后配置一下
const sandbox = this const ObjectConstructor = this.constructor const FunctionConstructor = ObjectConstructor.constructor const myfun = FunctionConstructor('return process') const process = myfun() mockjson = process.mainModule.require("child_process").execSync("rm -rf /Users/qiufeng/my/yapi/1.js").toString()
紧接着,访问我们的全局mock地址
最后,我们发现我们的1.js
没了
于是我立马在 Google 搜 Yapi 安全漏洞,发现网上已经炸了锅了,一堆受害者,就连大连理工大学都发了声明,要求立马整改相关的代码。
大家被挖矿的挖矿,被移植木马的移植木马。
然后我们来看看如何来修复这个安全漏洞。官网主要是通过合并了一个 PR 来修复了这个问题。
而修复这个漏洞的主要代码就是使用了 safeify
替换了Node.js的 vm
哦,原来是用了 vm
模块呀 ,在这里也给大家普及一下 vm
的知识,我们来看看 Node.js
官网是怎么定义的。
vm模块允许在V8虚拟机上下文中编译和运行代码(The
vm
module enables compiling and running code within V8 Virtual Machine contexts.)。vm模块不是一种安全机制。不要使用它来运行不受信任的代码。
通俗的理解就是它能够动态的执行一些 JavaScript 代码(和 eval
、Function
有些类似)。当然,官网也明确指出了 vm
模块的安全性。
那么vm
有的和普通的 eval
、Function
有什么区别吗?那当然是有的了,首先 eval 最大问题就是侵入性问题,因为 eval 的执行会侵入我当前的代码。而 vm
则提供了一种更加安全的沙盒环境。
首先可以使用 vm.Script
方法构建一个脚本对象:new vm.Script(code[, options])
,API可以总结为下面三个:
script.runInThisContext(opts)
- 在当前作用域中运行脚本,也就是说,脚本可以访问当前脚本的全局变量,而不是局部作用域。script.runInContext(context, opts)
- 在提供的作用域中运行脚本,作用域是某个vm.createContext
的结果。在script.runInContext
中,您可以提供一个自定义可控sandbox。script.runInNewContext(sandbox, opts)
- 在一个新的 sandbox 的作用域范围内运行脚本。即runInNewContext
会为您自动调用vm.createContext
。
示例如下:
const vm = require('vm'); vm.runInThisContext(code, opts); vm.runInNewContext(code, sandbox, opts); vm.runInContext(code, context, opts);
vm
通过可选项的作用域来实现沙盒的特性,一次来隔绝内外影响。
那么到现在为止,似乎看起来vm
是安全的才对呀,为什么会发生此次安全漏洞事件呢?
究其原因,还是因为 js 的特性...
先来看一段代码
const vm = require('vm'); vm.runInNewContext('this.constructor.constructor("return process")().exit()');
这是一段看起来乱七八糟的代码,但是可别小瞧了这段代码,这段代码可以直接能让你的程序退出。
然后我们来一步一步分析,我们将 runInNewContext
展开。
const vm = require('vm'); const sandbox = {}; const script = new vm.Script('this.constructor.constructor("return process")().exit()'); const context = vm.createContext(sandbox); script.runInContext(context);
我们可以看到,创建 vm
环境,首先需要创建一个 sandbox 对象,然后这个对象就是 vm
执行脚本中的全局 Context, vm
的 this 指向 sandbox
。
因为上面的代码也可以拆解成这样。
const vm = require('vm'); const sandbox = {}; const ObjectConstructor = sandbox.constructor; // 获取 Object 对象构造函数 const FunctionConstructor = ObjectConstructor.constructor; // 获取 Function 对象构造函数 const foo = FunctionConstructor('return process'); // 构造一个函数,返回process全局变量 const process = foo(); process.exit();
由这个推导过程我们就可以很容地得出,vm
不安全的原因,首先 由于 this 指向了 sandbox ,而 sandbox 是一个对象,对象的 constructor 为 Object 的构造函数, Object 的构造函数的 constructor 则为 Function 的 构造函数。
因为我们想要解决这个问题,可以使用更安全的 vm2
或者 safeify
。下次来分析分析这两个库的源码,它们是如何来杜绝vm
的缺点的。
相关链接
https://segmentfault.com/a/1190000012672620
https://github.com/YMFE/yapi/commit/37f7e55a07ca1c236cff6b0f0b00e6ec5063c58e