2021- 苍石- 在Javascript 中安全地执行动态脚本
在Javascript 中安全地执行动态脚本
动态脚本,在每种编程语言都有涉及,比如微软的
而第二种就是运行在服务端的动态脚本。由于客户端的脚本功能有限,很多强大的功能,比如操作文件,访问数据库都是没法访问的,为了解决这种问题,很多
eval 和new Function
这二者在
以
eval("process.exit()");
对于服务器环境而言,
function evalute(code, sandbox) {
sandbox = sandbox || Object.create(null);
const fn = new Function("sandbox", `with(sandbox){return (${code})}`);
const proxy = new Proxy(sandbox, {
has(target, key) {
// 让动态执行的代码认为属性已存在
return true;
},
});
return fn(proxy);
}
evalute("1+2"); // 3
evalute("console.log(1)"); // Cannot read property 'log' of undefined
这段代码会通过
NodeJS 中的其它选择?
如果只讨论服务端也就是
const vm = require("vm");
const x = 1;
const context = { x: 2 };
vm.createContext(context); // Contextify the object.
const code = "x += 40; var y = 17;";
// `x` and `y` are global variables in the context.
// Initially, x has the value 2 because that is the value of context.x.
vm.runInContext(code, context);
console.log(context.x); // 42
console.log(context.y); // 17
console.log(x); // 1; y is not defined.
从上述代码中可以看到,
try {
const script = new vm.Script("while(true){}", { timeout: 50 });
script.runInContext({});
// ....
} catch (err) {
//打印超时的 log
console.log(err.message);
}
const script = new vm.Script("setTimeout(()=>{},2000)", { timeout: 50 });
那么
const vm = require("vm");
vm.runInNewContext('this.constructor.constructor("return process")().exit()');
console.log("Never gets executed.");
通过运行以上代码,我们会发现,我们
由于
vm2
那我们就没办法在
而其中安全性较高并且功能更多的要数
const { VM } = require("vm2");
new VM().run('this.constructor.constructor("return process")().exit()');
// Throws ReferenceError: process is not defined
可以看到会抛出错误: process is not defined;在功能性上,
const { NodeVM } = require("vm2");
const vm = new NodeVM({
require: {
external: true,
},
});
vm.run(
`
var request = require('request');
request('http://www.google.com', function (error, response, body) {
console.error(error);
if (!error && response.statusCode == 200) {
console.log(body) // Show the HTML for the Google homepage.
}
})
`,
"vm.js"
);
但是,我们还是可以在
- 由于
vm2 中的NodeVM 不支持timeout 属性,while(true){} 会阻塞整个应用 - 即便在
vm2 中的VM 模块可以指定timeout ,和NodeJS 原生VM 一样,由于timeout 不能对异步代码生效,一旦运行异步代码,timeout 便失效了
safeify
那怎么解决这个问题呢?很多人看到这应该都能想到,可以通过进程去运行代码,然后如果超时便结束进程。是的,有了思路,发现
- 通过沙箱在子进程中运行脚本
- 通过进程池统一调度管理沙箱进程
- 处理的数据和结果返回给主进程
- 针对沙箱进程进行
CPU 、内存以及超时的限制
然而通过使用这个库,我又发现了很多问题:
- 其封装的是
vm2 中的VM 模块而不是NodeVM 模块,像在脚本中引入其它依赖是没法实现了 - 由于进程通信的限制,该模块将指定
context 中的方法的运行还是放在主进程中运行,没有完全的异步交给子进程
const safeVm = new Safeify({
timeout: 50, //超时时间,默认 50ms
asyncTimeout: 1000, //包含异步操作的超时时间,默认 500ms
quantity: 2, //沙箱进程数量,默认同 CPU 核数
memoryQuota: 100, //沙箱最大能使用的内存(单位 m),默认 500m
cpuQuota: 0.1, //沙箱的 cpu 资源配额(百分比),默认 50%
});
const context = {
a: 1,
b: 1,
add(a, b) {
while (true) {
// console.log(b)
}
return a + b;
},
};
(async function f() {
setTimeout(() => {
console.log("????");
}, 2000);
const rs = await Promise.all([safeVm.run(`return add(a,1)`, context)]);
console.log("result", rs);
// 释放资源
safeVm.destroy();
})();
像这样的逻辑会一直卡住主进程,
- 会在初始化时就实例化出配置的子进程,如果是
4 ,就会实例化4 条,对资源占用很不友好,并且需要手动调用safeify.destory() 方法去销毁子进程,由于执行脚本是异步的,对销毁时机需要很好的把握,一不小心就把还没执行完的子进程销毁掉了