简介
vm2是一个沙箱,可以使用列入白名单的Node的内置模块运行不受信任的代码。安全!
特征
- 使用您的代码并排在一个进程中安全地运行不受信任的代码
- 完全控制沙盒的控制台输出
- Sandbox对进程方法的访问权限有限
- 沙箱可以要求模块(内置和外置)
- 您可以限制对某些(或所有)内置模块的访问
- 您可以安全地调用方法并在沙箱之间交换数据和回调
- 免疫while (true) {}(这点应该是vm2可以设置超时时间,防止死循环一直执行)
- 对所有已知的攻击方法都免疫
- 运营商支持
它是如何工作的
- 它使用内部VM模块来创建安全上下文
- 它使用Proxies来防止逃离沙箱
- 它会覆盖builtin require来控制对模块的访问
Node的vm和vm2有什么区别?
const vm = require('vm');
vm.runInNewContext(
'this.constructor.constructor("return process")().exit()'
);
console.log('Never gets executed.');
const {VM} = require('vm2');
new VM().run(
'this.constructor.constructor("return process")().exit()'
);
// Throws ReferenceError: process is not defined
安装
npm i vm2
使用场景
在项目中需要处理用户输入的内容,限制用户可使用的模块,还需要对用户输入的内容检查错误,并及时反馈给用户。针对以上场景,选择使用vm2 模块中的 NodeVM 搭配 VMScript ,接下来做一个简单的介绍。
NodeVM
NodeVM
允许在常规Node
的上下文中使用相同的模块。
Option
- console- inherit启用控制台,redirect重定向到事件,off禁用控制台(默认值:) inherit。
- sandbox - VM的全局对象。
- compiler- javascript(默认)或coffeescript自定义编译器函数(接收代码及其文件路径)。
- eval-如果设置为false任何来电eval或函数的构造函数(Function,GeneratorFunction,等)将抛出一个EvalError(默认:true)。
- wasm- 如果设置为false任何尝试编译WebAssembly模块将抛出WebAssembly.CompileError(默认值:) true。
- sourceExtensions- 要作为源代码处理的文件扩展名数组(默认值:) [‘js’]。
- require- true或对象启用require方法(默认值:) false。
- require.external- true,允许的外部模块或对象的数组(默认值:) false。
- require.external.modules - 允许的外部模块阵列。还支持通配符,因此指定[’@scope/*-ver-??],例如,将允许使用具有以下形式的名称的所有模块@scope/something-ver-aa,@scope/other-ver-11等等。
- require.external.transitive- 布尔值,指示是否允许外部模块的传递依赖性(默认值:) false。
- require.builtin - 允许内置模块的数组,接受所有的[“*”](默认值:无)。
- require.root - 设置本地模块可被访问的路径(默认值:每个路径)。
- require.mock - 模拟模块的集合(外部或内置)。
- require.context- host(默认)要求主机中的模块并将它们代理到沙箱。sandbox在沙箱中加载,编译和需要模块。除了events在主机中始终需要并代理到沙箱之外的内置模块。
- require.import - 启动时要加载到NodeVM的模块数组。
- require.resolve - 如果在其中一个传统节点查找路径中找不到模块,则需要额外的查找功能。
- nesting- true启用VM嵌套(默认值:) false。
- wrapper- commonjs(默认)将脚本包装到CommonJS包装器中,none 检索脚本返回的值。
重要提示:超时对NodeVM无效,因此它不会免疫while (true) {}或类似的问题。
VMScript
你可以使用预编译的脚本来提高性能。预编译的VMScript可以多次运行。重要的是要注意代码没有绑定到任何VM(上下文); 相反,它在每次运行之前被绑定,仅用于该运行。(目前测试这个预编译只能检测出语法错误)
// script.js 在如下脚本存在两个错误:
// console拼写错错误,在括号里面hello右单引号丢失,
module.exports = function(){
consle.log('hello);
}
const {NodeVM, VMScript} = require('vm2');
const fs = require('fs');
const file = `${__dirname}\\script.js`;
const vm = new NodeVM({
console: 'inherit',
sandbox: {},
require: {
external: true,
builtin: ['fs', 'path'],
}
});
const script = new VMScript(fs.readFileSync(file), file);
script.compile()
在执行只能检测到拼写错误,缺少符号;如果把符号补上,就不会报错;检测不出使用模块未声明的错误。
错误处理
在运行沙箱中的代码时,最好用try/catch 进行包裹,这样出现问题也不会导致全局崩溃,代码编译和同步代码执行中的错误可由try/ catch 处理。可以通过将uncaughtException事件处理程序附加到Node 来处理异步代码执行中的错误process。
try {
var script = new VMScript("Math.random()").compile();
} catch (err) {
console.error('Failed to compile script.', err);
}
try {
vm.run(script);
} catch (err) {
console.error('Failed to execute script.', err);
}
process.on('uncaughtException', (err) => {
console.error('Asynchronous error caught.', err);
})
实例
这个实例主要使用了 vm2
如下一些常用功能:
- 打印功能
console
- 全局变量
sandbox
- 允许使用第三方库require.builtin
- 只读权限限制 freeze
// script.js
// 在此实例中使用了 sandbox 中的 module 和 crypto 两个模块,require 里的参数选项并没使用到。
module.exports = function () {
const crypto = require('crypto')
const secret = 'abcdefg';
const hash = crypto.createHmac('sha256', secret)
.update('I love cupcakes')
.digest('hex')
console.log(hash);
gdata["hash"] = hash;
};
// vm2/test.js
const { NodeVM, VMScript } = require('vm2');
const fs = require('fs');
const file = `${__dirname}/script.js`;
const util = {
rand () {
return Math.random() * 10;
}
}
console.log(file);
console.time('test')
let gdata = {};
const vm = new NodeVM({
console: 'inherit',
sandbox: { gdata },
require: {
external: true,
builtin: ['crypto'],
}
});
try {
var script = new VMScript(fs.readFileSync(file));
} catch (err) {
console.error('Failed to compile script.', err);
}
try {
vm.freeze(util, "util");
let res = vm.run(script);
res();
console.log(gdata);
} catch (err) {
console.error('Failed to execute script.', err);
}
process.on('uncaughtException', (err) => {
console.error('Asynchronous error caught.', err);
})
console.timeEnd('test')
console:
/*
c0fa1bc00531bd78ef38c628449c5102aeabd49b5dc3a2a516ea6ea959d6658e
9.638111982891482
{ hash:
'c0fa1bc00531bd78ef38c628449c5102aeabd49b5dc3a2a516ea6ea959d6658e',
randid: 9.638111982891482 }
test: 24.484ms
*/