作者:Bumpman-nobo背景在日常开发需求中,有时为了追求灵活性或降低开发难度,在业务代码中直接使用eval/Function/vm等函数,其中eval/Function可以看作是动态执行的JS,但是不能屏蔽当前执行环境的上下文。但是node.js提供了一个vm模块,相当于一个虚拟机,可以让你在执行代码的时候隔离当前的执行环境,避免被恶意代码攻击。vm基本介绍vm模块可以在V8虚拟机上下文中编译运行代码,虚拟机上下文可以自行配置,利用这个特性达到沙箱的效果。例如:constvm=require("vm");常量x=1;常量y=2;constcontext={x:2,console};vm.createContext(上下文);//上下文隔离对象。constcode="console.log(x);console.log(y)";vm.runInContext(code,context);//Output2//UncaughtReferenceError:yisnotdefined按照上面的例子,可以可见,和eval/Function最大的区别在于context是可以自定义的,可以控制执行代码的访问资源。比如上面的例子,除了语言语法、内置对象等,无法访问上下文之外的任何信息,所以例子中出现错误信息:yisundefined。下面是vm的执行示例图:Sandbox环境代码只能读取VM上下文数据。Sandboxescapenode.js在vm文档页面上有如下描述:vmmoduleisnotasecuremechanism。不要用它来运行不受信任的代码。当我第一次看到这句话的时候,我很好奇,为什么会这样呢?按照刚才的了解,他应该是安全的吧?经过搜索,我们发现了一个转义例子:constvm=require("vm");constctx={};vm.runInNewContext('this.constructor.constructor("returnprocess")().exit()',ctx);console.log("永远不会被执行。");上面的例子中,this指向ctx,通过原型链获取沙箱外的Funtion,完成转义,执行转义后的JS代码。上面的例子大致拆分为:tmp=ctx.constructor;//Objectexec=tmp.constructor;//Functionexec("返回进程");以上就是通过原型链完成越狱。如果上下文对象的原型链设置为null怎么办?constctx=Object.create(null);此时沙箱通过ctx.constructor时会报错,无法完成沙箱逃逸。完整的例子如下:constvm=require("vm");constctx=Object.create(null);vm.runInNewContext('this.constructor.constructor("returnprocess")().exit()',ctx);//throwError但是,真的有那么简单吗?我们来看下面这个成功转义的例子:constvm=require("vm");constctx=Object.create(null);ctx.data={};vm.runInNewContext('this.data.constructor.constructor("returnprocess")().exit()',ctx);//转义成功!console.log("永远不会被执行。");为什么会这样?原因是JS中所有对象的原型链都会指向Object.prototype,而Object.prototype和Function相互指向,所有对象都可以通过原型链得到Function,最终完成沙箱逃逸并执行代码.代码转义后可以执行如下代码获取require并加载其他模块函数,例如:constvm=require("vm");constctx={控制台,};vm.runInNewContext(`varexec=this.constructor.constructor;varrequire=exec('returnprocess.mainModule.constructor._load')();console.log(require('fs'));`,ctx);沙盒执行上下文是隔离的,但可以通过原型访问沙盒外的函数,可以通过chain方法获取,从而完成越狱,获取全局数据。示例图如下:综上所述,由于语言的特性,在沙盒环境下可以通过原型链获取全局Function,并通过它来执行代码。最后,正如官方所说,在使用vm的时候,你应该保证你运行的代码是可信的。eval/Function/vm等可以动态执行代码的函数必须用于在JavaScript中执行可信代码。以下可能是比较常见的使用脚本动态执行的场景:模板引擎、H5游戏、追求高度灵活配置的场景。解决方案的预处理,比如:代码安全扫描,语法限制使用vm2模块,其实质是通过proxy进行安全验证,虽然可能还是有逃逸方法没有出现,使用时要小心。自己实现解释器,并在解释器层接管所有对象创建和属性访问。欢迎关注傲兔实验室博客:aotu.io或关注傲兔实验室公众号(AOTULabs),不定期推送文章。
