在一些体系中,我们愿望给用户供应插进去自定义逻辑的才能,除了 RPC
和 REST
以外,运转客户供应的代码也是比较经常运用的要领,优点是能够极大地削减在收集上的耗时。JavaScript 是一种异常盛行而且轻易上手的言语,因而,让用户用 JavaScript 来写自定义逻辑是一个不错的挑选。下面我们引见 Node.js 供应的 vm 模块以及剖析用它来运转不信任代码能够碰到的题目。
vm 模块
vm 模块是 Node.js 内置的中心模块,它能让我们编译 JavaScript 代码和在指定的环境中运转。请看下面例子:
const util = require('util');
const vm = require('vm');
// 1. 建立一个 vm.Script 实例, 编译要实行的代码
const script = new vm.Script('globalVar += 1; anotherGlobalVar = 1; ');
// 2. 用于绑定到 context 的对象
const sandbox = {globalVar: 1};
// 3. 建立一个 context, 而且把 sandbox 这个对象绑定到这个环境, 作为全局对象
const contextifiedSandbox = vm.createContext(sandbox);
// 4. 运转上面编译的代码, context 是 contextifiedSandbox
const result = script.runInContext(contextifiedSandbox);
console.log(`sandbox === contextifiedSandbox ? ${sandbox === contextifiedSandbox}`);
// sandbox === contextifiedSandbox ? true
console.log(`sandbox: ${util.inspect(sandbox)}`);
// sandbox: { globalVar: 2, anotherGlobalVar: 1 }
console.log(`result: ${util.inspect(result)}`);
// result: 1
vm.Script
是一个类,用于建立代码实例,背面能够屡次运转。
vm.createContext(sandbox)
用于 “contextify” 一个对象,依据 ECMAScript 2015 言语范例,代码的实行须要一个 execution context。这里的 “contextify”,就是把传进去的对象与 V8 的一个新的 context 举行关联。这里所说的关联,我的明白是,这个 “contextified” 对象的属性将会成为谁人 context 的全局属性,同时,在 context 下运转代码时发生的全局属性也会成为这个 “contextified” 对象的属性。
script.runInContext(contextifiedSandbox)
就是使代码在 contextifiedSandbox
这个 context 中运转,从上面的输出能够看到,代码运转后,contextifiedSandbox
内里的属性的值已被改变了,运转效果是末了一个表达式的值。
除了上面几个接口以外,vm 模块另有一些更便利的接口,比方 vm.runInContext(code, contextifiedSandbox[, options])
,vm.runInNewContext(code[, sandbox][, options])
等,细致可看文档。
外层怎样获得代码运转效果
我们用 vm 运转代码的时刻极能够须要获得一些效果,从上面的例子中能够看到,我们能够经由过程把效果作为末了一个表达式的值传给外层,或许作为context
的属性给外层运用,这在同步代码里没有题目,然则假如效果须要依靠内里的异步操纵呢?这时候,我们能够经由过程在 context
里放一个回调函数。 下面是例子:
const util = require('util');
const vm = require('vm');
const sandbox = {globalVar: 1, setTimeout: setTimeout, cb: function(result) {
console.log(result);
}};
vm.createContext(sandbox);
const script = new vm.Script(`
setTimeout(function(){
globalVar++;
cb("async result");
}, 1000);
`,{});
script.runInContext(sandbox);
console.log(`globalVar: ${sandbox.globalVar}`);
// globalVar: 1
// async result
代码运转时候限制
script.runInContext(contextifiedSandbox[, options])
要领有一个 timeout
选项能够设定代码的运转时候,假如凌驾时候就会抛出毛病,请看下面例子:
const util = require('util');
const vm = require('vm');
const sandbox = {};
const contextifiedSandbox = vm.createContext(sandbox);
const script = new vm.Script('while(true){}');
const result = script.runInContext(contextifiedSandbox, {timeout: 1000});
// const result = script.runInContext(contextifiedSandbox, {timeout: 1000});
// ^
// Error: Script execution timed out.
再尝尝异步代码,
const util = require('util');
const vm = require('vm');
const sandbox = {globalVar: 1, setTimeout: setTimeout, cb: function(result) {
console.log(result);
}};
vm.createContext(sandbox);
const script = new vm.Script(`
setTimeout(function(){
globalVar++;
cb("async result");
}, 1000);
globalVar;
`,{});
const result = script.runInContext(sandbox, {timeout: 500});
console.log(`result: ${result}`);
// result: 1
// async result
没有毛病抛出,也就是说,这个选项并不能限制异步代码的运转时候,那应当怎样去限制一切代码的实行时候呢,现在彷佛没有接口停止 vm 代码的运转,假如有异步代码长时候不完毕,很轻易形成内存泄漏,现在可行的计划是运用子历程去运转代码,假如凌驾限制时候还没有效果,就杀掉该子历程,别的,运用子历程还能够更方便地对内存等资本举行限制。
定制 context 与平安题目
在一个全新的 V8 context 里运转代码,内里包含了言语范例划定的内置的一些函数和对象,假如我们想要一些言语范例以外的功用或许模块,我们须要把响应对象放到与这个 context 关联的对象里,比方在上面例子中的这句代码:
const sandbox = {globalVar: 1, setTimeout: setTimeout, cb: function(result) {
console.log(result);
}};
setTimeout
不是言语范例划定的内置函数, context 自身不供应,所以我们须要经由过程关联的对象传进去。
但是,当我们把一些模块功用供应给 context 的时刻,也同时带入了更多的平安隐患,请看下面来自例子:
const util = require('util');
const vm = require('vm');
const sandbox = {};
vm.createContext(sandbox);
const script = new vm.Script(`
// sandbox 的 constructor 是外层的 Object 类
// Object 类的 constructor 是外层的 Function 类
const OutFunction = this.constructor.constructor;
// 因而, 应用外层的 Function 组织一个函数就能够获得外层的全局 this
const OutThis = (OutFunction('return this;'))();
// 获得 require
const require = OutThis.process.mainModule.require;
// 尝尝
require('fs');
`,{});
const result = script.runInContext(sandbox);
console.log(result === require('fs'));
// true
明显,定制 context 的时刻,任何一个传进去的对象或许函数都能够带来上面的题目,平安题目真的有许多事情须要做。
Github 上有一些开源的模块用于运转不信任代码,比方 sandbox,vm2,jailed等。检察这些项目的 issue 能够发明,sandbox 和 jailed 都能够用相似上面的要领打破限制,而 vm2 对这方面做了防护,别的方面也做了更多的平安事情,相对平安些。
生产中能够斟酌在子历程中运转 vm2, 然后增添更低层的平安限制, 比方限制历程的权限和运用 cgroups 举行 IO,内存等资本限制,这里不细致议论。
总结
本文经由过程几个例子引见了 Node.js 的 vm 模块以及运用 vm 模块运转不信任代码能够碰到的题目,而且对平安题目给出了一些发起。
参考
vm
Allowing to terminate a vm context/script
V8 Embedder’s Guide
ECMAScript 2015 言语范例
sandbox/issues/50
vm2/issues/32
jailed/issues/33
cgroups