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

如何安全执行自定义nodejs脚本

时间:2023-04-03 17:14:15 Node.js

最近在业务开发过程中,遇到了需要执行自定义nodejs脚本的场景,那么如何安全执行自定义神秘代码呢?用户脚本用户脚本的特点是不可预测。在浏览器端,用户脚本可以直接获取cookies、localStorage等信息;在Node端,可以进行退出进程、删除文件等危险操作,存在安全隐患。因此,我们假设默认情况下用户提供的脚本内容是不可信任的。那么这样一个不可信的脚本应该如何执行呢?如何执行以node环境为例,我们在不考虑安全性的情况下,简单执行一个脚本一般有两种方式://nodev14.16.0eval('process.exit()');console.log('processhasexitbyeval')newFunction('process.exit()')()console.log('processhasexitbynewFunction')其中eval和newFunction函数都将在当前上下文中执行,并且可以执行一些危险的操作又会影响当前的运行环境。因此,需要将执行用户脚本的环境与当前环境隔离开来,使用沙箱来执行用户脚本,这样可以避免用户脚本影响当前执行上下文。这里我们只从执行环境的角度考虑,脚本的语法分析和过滤不在本文讨论范围之内。使用webworker一提到sandbox,首先想到的就是worker,那么在worker中执行用户代码是否可行呢?看这个例子:const{Worker,isMainThread,parentPort}=require('worker_threads');if(isMainThread){console.log('在主线程');constworker=newWorker(__文件名);//用户脚本constscript=`constfs=require('fs');fs.unlink('./test.txt',(e)=>{if(e)throwe;console.log('filewasdeleted')})`;worker.postMessage(script);}else{console.log('inworkerthread');parentPort.on('message',(message)=>{eval(message);})}//执行结果://filewasdeleted上面的例子可以看出,虽然用户代码存在安全隐患转移到worker线程,无法阻止用户脚本在node端调用fs模块进行文件相关的危险操作。您可以使用网络工作者。虽然在工作线程中无法直接获取cookie、localStorage、DOM等数据,但由于postMessage可以接收到任务来源信息,这将成为XSS攻击的潜在风险点。因此,在浏览器端使用时,需要对服务器端的信息进行相应的过滤清洗。使用child_processchild_process是Nodejs中创建的子进程,可以直接执行shell命令。使用child_process遇到的问题和webworker遇到的问题类似,就不展开了。为了使用vm模块,vm是Node中的一个模块,可以在v8虚拟机中运行你的代码。是沙盒隔离环境,默认屏蔽了进程、控制台、fs等全局对象,具有一定的安全性保证:constvm=require('vm');//ReferenceError:processisnotdefinedvm.runInNewContext('process.exit()');也可以在vm中选择脚本的执行上下文:vm.runInThisContext//在当前的Context中执行vm.runInContext//在指定的上下文中运行脚本,上下文就是vm.createContext中返回的结果vm.runInNewContext//创建一个新的context执行,默认会执行vm.createContext方法vm.runInNewContext和vm.createContext也支持传入一些全局对象供脚本使用的contextconstvm=require('vm');//将当前上下文的进程传入新上下文,此时进程成功退出vm.runInNewContext('process.exit()',{process,});表面上看起来很完美,但实际上vm模块存在一些已知的安全问题,正如Node官方文档中所写:vm模块不是一种安全机制。不要用它来运行不受信任的代码。以下面的代码为例,通过访问上层的构造函数,可以脱离vm构造的沙箱环境,“呼吸沙箱外面的空气”:constvm=require('vm');//的构造函数sandbox是外层的Object类//Object类的构造函数是外层的FunctionClass//func=this.constructor.constructor//因此使用外层Function构造函数可以获取外层全局环境的上下文//process=(func('returnthis;'))().process;vm.runInNewContext('this.constructor.constructor("返回过程")().exit()');console.log('永远不会被执行。');vm2隔离运行在社区有很多解决方案相对于sandbox、vm2、jailed等用户脚本,vm2在安全性方面做了更多的工作,相对可靠。vm2主要使用Proxy进行上下文同步,防止代码逃逸,实现对全局对象和内置模块的访问限制,重写require方法实现对模块的访问管理。上面的转义例子是在vm2中截获的:const{VM}=require('vm2');newVM().run('this.constructor.constructor("returnprocess")().exit()');//ThrowsReferenceError:processisnotdefined在vm2中,新增了一个NodeVM类来实现node中的模块化调用,因此可以方便的导出脚本中的一些内容:const{NodeVM}=require('vm2');constscript=`module.exports=async()=>{return1}`;constfn=newNodeVM().run(script);fn();//Result:1//fn是module.exports返回的函数,但是vm2没有解决vm中的一些已知问题:对于NodeVM模块,不支持超时配置,不能处理while(true)等死循环{},会导致进程卡住无法退出。对于VM模块,timeout超时只能对同步代码生效,不能处理异步超时处理。同步代码不支持超时。可以使用vm2的VM模块或者原来的vm模块手动实现。支持timeout参数处理同步场景下的超时配置。代码如下://vm2functionfnSyncTimeout(fn,timeout){//...returnnewVM({timeout,sandbox:{fn}}).run('fn()')}当处理线程为不阻塞,类似于接口调用场景我们可以使用Promise.race来处理异步超时:functionfnAsyncTimeout(fn,timeout){lettimer;returnPromise.race([fn(),newPromise((res,rej)=>{timer=setTimeout(()=>{rej('scripttimeouterror')},timeout)})]).finally(()=>{clearTimeout(timer);});;}上面的方法虽然可以解决单一场景,但是由于单线程的特点nodejs,对于无法返回的异步脚本的处理是没有用的。例如,如果异步脚本包含死循环,则当前线程将被阻塞,即使有异步回调也无法继续执行。而且vm模块创建后不能手动关闭,内置超时配置只支持同步脚本。基于以上问题,我想到了结合webworker来优化处理。主要思想是在工作线程中执行用户脚本以避免阻塞主线程,并在主线程中配置一个计时器,当超时时间到时手动退出工作线程。主要实现如下:const{isMainThread,Worker,parentPort}=require("worker_threads");//超时consttimeout=60000;if(isMainThread){constw=newWorker(__filename);让checkEndTimer;w.on("在线",()=>{checkEndTimer=setTimeout(()=>{w.terminate();},超时);console.log('脚本开始')});w.on("退出",()=>{clearTimeout(checkEndTimer);console.log('脚本结束');});w.on('message',(msg)=>{//处理脚本结果});}else{const{NodeVM,VMScript}=require("vm2");constvm=newNodeVM();//用户脚本constcode=`module.exports=async()=>{awaitnewPromise(res=>setTimeout(res,2000))//死循环while(true){}return1;}`;尝试{newVMScript(code).compile();}catch(err){console.error('编译sc失败撕裂。',错误);}constfn=vm.run(代码);(async()=>{constdata=awaitfn();parentPort.postMessage(data);})()}总结目前使用工作线程+vm2作为执行用户脚本的解决方案进行了优化。与其他方法相比,它似乎提供了更强大的沙箱,但不排除新的安全隐患尚未被发现。总之,代码安全无小事,再考虑一下不要太多~参考vm2https://github.com/patriksimek/vm2/issues/180NodeJs-vmSandboxingNodeJSishard,这里是为什么