做者: 凹凸曼 - nobonode
平常开发需求中有时候为了追求灵活性或下降开发难度,会在业务代码里直接使用 eval/Function/vm 等功能,其中 eval/Function 算是动态执行 JS,但没法屏蔽当前执行环境的上下文,但 node.js 里提供了 vm 模块,至关于一个虚拟机,可让你在执行代码时候隔离当前的执行环境,避免被恶意代码攻击。安全
vm 模块可在 V8 虚拟机上下文中编译和运行代码,虚拟机上下文可自行配置,利用该特性作到沙盒的效果。例如:ui
const vm = require("vm"); const x = 1; const y = 2; const context = { x: 2, console }; vm.createContext(context); // 上下文隔离化对象。 const code = "console.log(x); console.log(y)"; vm.runInContext(code, context); // 输出 2 // Uncaught ReferenceError: y is not defin
根据以上示例,能够看出和 eval/Function 最大的区别就是可自定义上下文,也就能够控制被执行代码的访问资源。例如以上示例,除了语言的语法、内置对象等,没法访问到超出上下文外的任何信息,因此示例中出现了错误提示: y 未定义。如下是 vm 的的执行示例图:this
沙盒环境代码只能读取 VM 上下文 数据。spa
node.js 在 vm 的文档页上有以下描述:prototype
vm 模块不是安全的机制。 不要使用它来运行不受信任的代码。代理
刚开始看到这句话的很好奇,为何会这样?按照刚才的理解他应该是安全的?搜索后咱们找到一段逃逸示例:code
const vm = require("vm"); const ctx = {}; vm.runInNewContext( 'this.constructor.constructor("return process")().exit()', ctx ); console.log("Never gets executed.");
以上示例中 this 指向 ctx 并经过原型链的方式拿到沙盒外的 Funtion,完成逃逸,并执行逃逸后的 JS 代码。对象
以上示例大体拆分:blog
tmp = ctx.constructor; // Object exec = tmp.constructor; // Function exec("return Process");
以上是经过原型链方式完成逃逸,若是将上下文对象的原型链设置为 null 呢?
const ctx = Object.create(null);
这时沙盒在经过 ctx.constructor,就会出错,也就没法完成沙盒逃逸,完整示例以下:
const vm = require("vm"); const ctx = Object.create(null); vm.runInNewContext( 'this.constructor.constructor("return process")().exit()', ctx ); // throw Error
但,真的这样简单吗?
再来看看如下成功逃逸示例:
const vm = require("vm"); const ctx = Object.create(null); ctx.data = {}; vm.runInNewContext( 'this.data.constructor.constructor("return process")().exit()', ctx ); // 逃逸成功! console.log("Never gets executed.");
为何会这样?
缘由
因为 JS 里全部对象的原型链都会指向 Object.prototype,且 Object.prototype 和 Function 之间是相互指向的,全部对象经过原型链都能拿到 Function,最终完成沙盒逃逸并执行代码。
逃逸后代码能够执行以下代码拿到 require,从而并加载其余模块功能,示例:
const vm = require("vm"); const ctx = { console, }; vm.runInNewContext( ` var exec = this.constructor.constructor; var require = exec('return process.mainModule.constructor._load')(); console.log(require('fs')); `, ctx );
沙盒执行上下文是隔离的,但可经过原型链的方式获取到沙盒外的 Function,从而完成逃逸,拿到全局数据,示例图以下:
因为语言的特性,在沙盒环境下经过原型链的方式能获取全局的 Function,并经过它来执行代码。
最终确实如官方所说,在使用 vm 的时应确保所运行的代码是可信任的。
eval/Function/vm 等可动态执行代码的功能在 JavaScript 里必定是用来执行可信任代码。
如下多是比较常见会用到动态执行脚本的场景:模板引擎,H5 游戏、追求高度灵活配置的场景。
欢迎关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。