node 官方文档里提到 node 的 vm 模块可以用来做沙箱环境执行代码, 对代码的上下文环境做隔离.
\A common use case is to run the code in a sandboxed environment. The sandboxed code uses a different V8 Context, meaning that it has a different global object than the rest of the code.
先看一个例子
- const vm = require('vm');
- let a = 1;
- var result = vm.runInNewContext('var b = 2; a = 3; a + b;', {a});
- console.log(result); // 5
- console.log(a); // 1
- console.log(typeof b); // undefined
沙箱环境中执行的代码对于外部代码没有产生任何影响, 无论是新声明的变量 b, 还是重新赋值的变量 a. 注意最后一行的代码默认会被加上 return 关键字, 因此无需手动添加, 一旦添加的话不会静默忽略, 而是执行报错.
- const vm = require('vm');
- let a = 1;
- var result = vm.runInNewContext('var b = 2; a = 3; return a + b;', {a});
- console.log(result);
- console.log(a);
- console.log(typeof b);
如下所示
- evalmachine.<anonymous>:1
- var b = 2; a = 3; return a + b;
- ^^^^^^
- SyntaxError: Illegal return statement
- at new Script (vm.js:74:7)
- at createScript (vm.js:246:10)
- at Object.runInNewContext (vm.js:291:10)
- at Object.<anonymous> (/Users/xiji/workspace/learn/script.js:3:17)
- at Module._compile (internal/modules/cjs/loader.js:678:30)
- at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
- at Module.load (internal/modules/cjs/loader.js:589:32)
- at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
- at Function.Module._load (internal/modules/cjs/loader.js:520:3)
- at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)
除了 runInNewContext 外, vm 还提供了 runInThisContext 和 runInContext 两个方法都可以用来执行代码 runInThisContext 无法指定 context
- const vm = require('vm');
- let localVar = 'initial value';
- const vmResult = vm.runInThisContext('localVar +="vm";');
- console.log('vmResult:', vmResult);
- console.log('localVar:', localVar);
- console.log(global.localVar);
由于无法访问本地的作用域, 只能访问到当前的 global 对象, 因此上面的代码会因为找不到 localVal 而报错
- evalmachine.<anonymous>:1
- localVar += "vm";
- ^
- ReferenceError: localVar is not defined
- at evalmachine.<anonymous>:1:1
- at Script.runInThisContext (vm.js:91:20)
- at Object.runInThisContext (vm.js:298:38)
- at Object.<anonymous> (/Users/xiji/workspace/learn/script.js:3:21)
- at Module._compile (internal/modules/cjs/loader.js:678:30)
- at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
- at Module.load (internal/modules/cjs/loader.js:589:32)
- at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
- at Function.Module._load (internal/modules/cjs/loader.js:520:3)
- at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)
如果我们把要执行的代码改成直接赋值的话就可以正常运行了, 但是也产生了全局污染 (全局的 localVar 变量)
- const vm = require('vm');
- let localVar = 'initial value';
- const vmResult = vm.runInThisContext('localVar ="vm";');
- console.log('vmResult:', vmResult); // vm
- console.log('localVar:', localVar); // initial value
- console.log(global.localVar); // vm
runInContext 在传入 context 参数上与 runInNewContext 有所区别 runInContext 传入的 context 对象不为空而且必须是经 vm.createContext() 处理过的, 否则会报错. runInNewContext 的 context 参数是非必须的, 而且无需经过 vm.createContext 处理. runInNewContext 和 runInContext 因为有指定 context, 所以不会向 runInThisContext 那样产生全局污染 (不会产生全局的 localVar 变量)
- const vm = require('vm');
- let localVar = 'initial value';
- const vmResult = vm.runInNewContext('localVar ="vm";');
- console.log('vmResult:', vmResult); // vm
- console.log('localVar:', localVar); // initial value
- console.log(global.localVar); // undefined
当需要一个沙箱环境执行多个脚本片段的时候, 可以通过多次调用 runInContext 方法但是传入同一个 vm.createContext() 返回值实现.
超时控制及错误捕获
vm 针对要执行的代码提供了超时机制, 通过指定 timeout 参数即可以 runInThisContext 为例
- const vm = require('vm');
- let localVar = 'initial value';
- const vmResult = vm.runInThisContext('while(true) { 1 }; localVar ="vm";', { timeout: 1000});
- vm.js:91
- return super.runInThisContext(...args);
- ^
Error: Script execution timed out.
- at Script.runInThisContext (vm.js:91:20)
- at Object.runInThisContext (vm.js:298:38)
- at Object.<anonymous> (/Users/xiji/workspace/learn/script.js:3:21)
- at Module._compile (internal/modules/cjs/loader.js:678:30)
- at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
- at Module.load (internal/modules/cjs/loader.js:589:32)
- at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
- at Function.Module._load (internal/modules/cjs/loader.js:520:3)
- at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)
- at startup (internal/bootstrap/node.js:228:19)
可以通过 try catch 来捕获代码错误
- const vm = require('vm');
- let localVar = 'initial value';
- try {
- const vmResult = vm.runInThisContext('while(true) { 1 }; localVar ="vm";', {
- timeout: 1000
- });
- } catch(e) {
- console.error('executed code timeout');
- }
延迟执行
vm 除了即时执行代码之外, 也可以先编译然后过一段时间再执行, 这就需要提到 vm.Script 了. 其实无论是 runInNewContext,runInThisContext 还是 runInThisContext, 背后其实都创建了 Script, 从之前的报错信息就可以看出来 接下来我们就用 vm.Script 来重写本文开头的例子
- const vm = require('vm');
- let a = 1;
- var script = new vm.Script('var b = 2; a = 3; a + b;');
- setTimeout(() => {
- let result = script.runInNewContext({a});
- console.log(result); // 5
- console.log(a); // 1
- console.log(typeof b); // undefined
- }, 300);
除了 vm.Script,node 在 9.6 版本中新增了 vm.Module 也可以做到延迟执行, vm.Module 主要用来支持 ES6 module, 而且它的 context 在创建的时候就已经绑定好了, 关于 vm.Module 目前还需要在命令行使用 flag 来启用支持
node --experimental-vm-module index.js
vm 作为沙箱环境安全吗?
vm 相对于 eval 来说更安全一些, 因为它隔离了当前的上下文环境了, 但是尽管如此依然可以访问标准的 JS API 和全局的 NodeJS 环境, 因此 vm 并不安全, 这个在官方文档里就提到了
The vm module is not a security mechanism. Do not use it to run untrusted code
请看下面的例子
- const vm = require('vm');
- vm.runInNewContext("this.constructor.constructor('return process')().exit()")
- console.log("The app goes on...") // 永远不会输出
为了避免上面这种情况, 可以将上下文简化成只包含基本类型, 如下所示
- let ctx = Object.create(null);
- ctx.a = 1; // ctx 上不能包含引用类型的属性
- vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);
针对原生 vm 存在的这个问题, 有人开发了 vm2 包, 可以避免上述问题, 但是也不能说 vm2 就一定是安全的
- const {VM} = require('vm2');
- new VM().run('this.constructor.constructor("return process")().exit()');
虽然执行上述代码没有问题, 但是由于 vm2 的 timeout 对于异步代码不起作用, 所以下面的代码永远不会执行结束.
- const { VM } = require('vm2');
- const vm = new VM({ timeout: 1000, sandbox: {}});
- vm.run('new Promise(()=>{})');
即使希望通过重新定义 Promise 的方式来禁用 Promise 的话, 还是一个可以绕过的
- const { VM } = require('vm2');
- const vm = new VM({
- timeout: 1000, sandbox: { Promise: function(){}}
- });
- vm.run('Promise = (async function(){})().constructor;new Promise(()=>{});');
来源: http://www.jb51.net/article/140134.htm