当前位置: 首页 > Web前端 > JavaScript

大佬20行代码引发的“JavaScript沙箱”思考,逐步搭建类似Codepen的沙箱环境~

时间:2023-03-26 23:19:32 JavaScript

原文请参考我的文章公众号引发的“JavaScript沙箱”20linesofcode思考,逐步打造一个类似于Codepen的沙盒环境~SandboxSandbox允许其中包含的代码以适当的方式执行或用于测试,但不会对主程序或其他代码库(意外或恶意的))造成任何损害的容器称为沙箱。本文将通过with()、Proxy()、Function()、iframe技术实现一个类似Codepen的JS沙箱环境。如何实现沙箱环境?沙盒环境的目的是限制程序代码在安全不影响主程序的环境中运行。要达到这样的效果,可以从一个可靠的、自实现的上下文中限制程序中访问变量的来源,即需要为要执行的程序构造一个作用域。使用eval函数实现一个简单的沙盒环境原型//执行上下文对象constctx={func:variable=>{console.log(variable)},foo:'foo'}//沙盒原型函数babySandbox(code){eval(code)//构造一个执行程序的函数作用域}//要执行的程序constcode=`ctx.foo='bar'ctx.func(ctx.foo)`babySandbox(code)//bar但是有一个小问题,代码中的代码必须加上ctx。(访问变量的命名空间)去访问具体的变量,非常不方便,而且我们无法控制沙箱内部代码的写法。但也有一个解决方案。这里可以使用with语句改变不使用命名空间的变量作用域链的顶级对象,从而去掉ctx。关于with语句,当JavaScript发现一个没有使用命名空间的变量时,会通过作用域链去寻找,作用域链与代码执行的上下文或包含这个变量的函数有关。with语句将一个对象添加到作用域链的顶部,with语句中的代码首先尝试从提供给它的沙箱对象中找到变量。如果语句中存在与作用域链中属性同名的未命名变量,则该变量将指向属性值。如果没有同名属性,将抛出ReferenceError异常。我们先来看一个MDN上的with语句的例子:下面的with语句指定Math对象为默认对象。with语句中的变量指向Math对象的PI、cos、sin函数,前面不需要加命名空间(Math.)。所有后续引用都指向Math对象。vara,x,y;varr=10;with(Math){a=PI*r*r;x=r*余弦(圆周率);y=r*sin(PI/2);}withstatementblessing,去除“需要使用命名空间访问变量”的问题varsb='globalsb'//执行上下文对象constctx={func:console.log,sb:'ctxsb'}//沙盒函数使用withuseWithSandbox(code,ctx){//添加with包,使用我们指定的ctx作为顶级作用域with(ctx){eval(code)}}//要执行的程序constcode=`func(sb)`useWithSandbox(code,ctx);//ctxsb这样,执行程序中的变量就来自提供的上下文ctx。但是如果在提供的ctx中没有找到某个变量,代码还是会沿着作用域链向上查找,这样的沙箱环境仍然无法控制内部代码的执行。比如对上面的代码稍作修改,观察执行结果。varsb='globalsb'constctx={func:console.log,//sb:'ctxsb',//--在提供给代码的上下文中注释掉sb}functionuseWithSandbox(code,ctx){with(ctx){eval(code)}}constcode=`func(sb)`useWithSandbox(code,ctx);//globalsb那么如何实现“沙箱中的代码只查找我们提供的上下文中的变量”呢?借助ES6的新特性Proxy,“通过has拦截属性的访问,控制变量查找的范围”。增加了proxy()来锁定沙箱中执行代码的可访问范围(禁止在全局范围内搜索变量)Proxy中的get和set方法只能拦截代理对象中已经存在的属性,这两个钩子不知道代理对象中不存在的属性。因此需要使用Proxy.has()来拦截对with代码块中任何变量的访问。先看一个初级版本varsb='globalsb'constctx={func:console.log,sb:'ctxsb',}functionuseWithSandbox(code,ctx){with(ctx){eval(code)}}constcode=`func(sb)`varproxyCtx=newProxy(ctx,{//!!!has可以拦截对with代码块(在运算符中)中的任何属性的访问has:(target,prop)=>{if(!target.hasOwnProperty(prop)){thrownewError(`无效表达式-[${prop}]!`)}returntrue;}})useWithSandbox(code,proxyCtx);//错误...执行程序后报错:UncaughtError:Invalidexpression-[eval]!这是因为“has会拦截对with代码块中所有变量的访问”,而with语句中包含的eval函数显然没有在我们提供给with的上下文(ctx)中定义。如果只监控执行代码中的程序,需要转换手动执行代码的形式,让with语句只包含要执行的代码内容。请参阅编写JavaScript框架–沙盒代码评估。通过返回一个包含withwithnewFunction的函数实例来实现。Function()支持,使得with语句只包含要执行的代码/***将包含with语句的拼接代码片段作为Function的函数体,*这样,当代理作为沙箱,只有程序中执行的代码是被监控的目标。*/functioncompileCode(src){src='with(sandbox){'+src+'}'letcode=newFunction('sandbox',src)/**上面代码返回的函数实例的效果是如下*///?anonymous(sandbox){//with(sandbox){//...only-usercode...//要执行的代码//}//}/**以上是代码函数*/returnfunction(sandbox){returncode(newProxy(sandbox,{has}))}}//inoperatorthatinterceptsobjectaccessfunctionhas(target,key){returntrue}letcode=`returna+b`letctx={a:1,b:2}console.log(compileCode(code)(ctx))//3newFunction()与eval()有以下两点不同。newFunction()只对代码求值一次,调用它返回的函数不会对代码再次求值,效率更高;newFunction()不能访问局部封闭变量,但仍然可以访问全局范围;对于我们的用例,newFunction()是eval()的更好替代品。具有更高的性能和安全性,再配合Proxy防止全局访问,让沙箱趋向于一个完整体。iframe支持实现类似codepen的沙箱环境来动态创建iframe。借助DOMHTMLIFrameElement对象,脚本可以通过contentWindow访问内嵌框架的window对象。contentDocumen属性指的是