当前位置: 首页 > 后端技术 > Node.js

Node.js的vm模块及运行不可信代码分析

时间:2023-04-03 15:57:41 Node.js

在某些系统中,我们希望为用户提供插入自定义逻辑的能力。除了RPC和REST,运行客户提供的代码也是一种常用的方法,好处是可以大大减少花在网络上的时间。JavaScript是一种非常流行且易于学习的语言,因此让用户使用JavaScript编写自定义逻辑是一个不错的选择。下面我们介绍一下Node.js提供的vm模块,分析使用它运行非信任代码时可能遇到的问题。vm模块vm模块是Node.js内置的核心模块,它允许我们编译JavaScript代码并在指定的环境中运行。请看下面的例子:constutil=require('util');constvm=require('vm');//1.创建一个vm.Script实例,编译要执行的代码constscript=newvm.Script('globalVar+=1;anotherGlobalVar=1;');//2.对象constsandbox={globalVar:1};//3.创建一个上下文,并将沙箱对象绑定到这个环境,作为一个全局对象constcontextifiedSandbox=vm.createContext(sandbox);//4.运行上面编译的代码,context是contextifiedSandboxconstresult=script.runInContext(contextifiedSandbox);console.log(`sandbox===contextifiedSandbox?${sandbox===contextifiedSandbox}`);//sandbox===contextifiedSandbox?trueconsole.log(`sandbox:${util.inspect(sandbox)}`);//sandbox:{globalVar:2,anotherGlobalVar:1}console.log(`result:${util.inspect(result)}`);//result:1vm.Script是用来创建代码实例的类,以后可以多次运行。vm.createContext(sandbox)用于“上下文化”一个对象。根据ECMAScript2015语言规范,代码执行需要执行上下文。这里的“contextify”是将传入的对象关联到一个新的V8上下文中。这里说的association,我的理解是这个“contextified”对象的属性会成为那个context的全局属性,同时代码在context下运行时产生的全局属性也会成为那个context的属性这个“上下文化”的对象。script.runInContext(contextifiedSandbox)是让代码运行在contextifiedSandbox的上下文中。从上面的输出我们可以看到,代码运行后,contextifiedSandbox中的属性值发生了变化,运行的结果就是最后一个表达式的值。除了以上接口,vm模块还有一些比较方便的接口,比如vm.runInContext(code,contextifiedSandbox[,options]),vm.runInNewContext(code[,sandbox][,options])等,详情可参见文档。外层如何获取代码运行结果?当我们使用vm运行代码时,我们可能需要得到一些结果。从上面的例子可以看出,我们可以把结果作为最后一个表达式的值传递给外层,或者作为一个context属性给外层使用,这在同步代码中是没有问题的,但是呢如果结果需要依赖里面的异步操作?这时候,我们可以在上下文中放一个回调函数。这是一个例子:constutil=require('util');constvm=require('vm');constsandbox={globalVar:1,setTimeout:setTimeout,cb:function(result){console.log(result);}};vm.createContext(sandbox);constscript=newvm.Script(`setTimeout(function(){globalVar++;cb("asyncresult");},1000);`,{});script.runInContext(沙盒);console.log(`globalVar:${sandbox.globalVar}`);//globalVar:1//异步结果代码运行时间限制script.runInContext(contextifiedSandbox[,options])方法有一个超时选项可以设置代码的时间,超过时间会报错,请看下面的例子: constutil=require('util');constvm=require('vm');constsandbox={};constcontextifiedSandbox=vm.createContext(sandbox);constscript=newvm.Script('while(true){}');constresult=script.runInContext(contextifiedSandbox,{timeout:1000});//const结果=script.runInContext(contextifiedSandbox,{timeout:1000});//^//错误:脚本执行超时。再次尝试异步代码,constutil=require('util');常量虚拟机=require('vm');constsandbox={globalVar:1,setTimeout:setTimeout,cb:function(result){console.log(result);}};vm.createContext(sandbox);constscript=newvm。Script(`setTimeout(function(){globalVar++;cb("asyncresult");},1000);globalVar;`,{});constresult=script.runInContext(sandbox,{timeout:500});控制台。log(`result:${result}`);//result:1//asyncresult没有抛出错误,也就是说这个选项不限制异步代码的运行时间,那么如何限制执行allcode至于当时好像没有终止vm代码运行的接口。如果有长时间不结束的异步代码,很容易造成内存泄漏。目前可行的方案是使用子进程来运行代码。如果超过时间限制还没有结果,则kill掉。另外,使用子进程也可以更方便的限制内存等资源。自定义上下文和安全问题。在新的V8上下文中运行代码,其中包含语言规范规定的一些内置函数和对象。如果我们想要一些语言规范之外的函数或者模块,我们需要把对应的对象放到这个context关联的对象中,比如上面例子中的代码:constsandbox={globalVar:1,setTimeout:setTimeout,cb:function(result){console.log(result);}};setTimeout不是语言规范规定的内置函数,context本身也没有提供,需要通过关联对象传入。但是,当我们为上下文提供一些模块功能时,也带来了更多的安全隐患。请看下面的例子:constutil=require('util');constvm=require('vm');constsandbox={};vm.createContext(sandbox);constscript=newvm.Script(`//沙盒的构造函数是外层的Object类//Object的构造函数是外层的Function类constOutFunction=this.constructor.constructor;//所以使用外层Function构造函数可以得到外层的全局thisconstOutThis=(OutFunction('returnthis;'))();//获取requireconstrequire=OutThis.process.mainModule.require;//尝试require('fs');`,{});constresult=script.runInContext(sandbox);console.log(result===require('fs'));//true很明显,在自定义context的时候,传入任何一个对象或者函数,都可能导致上面的问题,确实需要做很多工作在安全问题上完成。Github上有一些运行不受信任代码的开源模块,如sandbox、vm2、jailed等,查看这些项目的issues可以发现,sandbox和jailed都可以使用类似的方法来突破限制,而vm2在这方面做了保护,其他方面也做了更多的安全工作,相对安全。在生产中,可以考虑在子进程中运行vm2,然后添加更底层的安全限制,比如限制进程权限,使用cgroups进行IO、内存等资源限制,这里不做详细讨论。总结本文通过几个实例介绍了Node.js的vm模块以及使用vm模块运行非信任代码时可能遇到的问题,并针对安全问题给出了一些建议。参考vmAllowingtoterminateavmcontext/scriptV8Embedder'sGuideECMAScript2015LanguageSpecificationsandbox/issues/50vm2/issues/32jailed/issues/33cgroups