一文彻底搞懂前端沙箱

网站建设4年前发布
23 0 0

也称作:“沙箱/沙盒/沙盘”。沙箱是一种安全机制,为运行中的程序提供隔离环境。通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用。沙箱能够安全的执行不受信任的代码,且不影响外部实际代码影响的独立环境。,在一些应用中,我们希望给用户提供插入自定义逻辑的能力,比如 Microsoft 的 Office 中的 VBA,比如一些游戏中的 lua 脚本,FireFox 的「油猴脚本」,能够让用户发在可控的范围和权限内发挥想象做一些好玩、有用的事情,扩展了能力,满足用户的个性化需求。,大多数都是一些客户端程序,在一些在线的系统和产品中也常常也有类似的需求,事实上,在线的应用中也有不少提供了自定义脚本的能力,比如 Google Docs 中的 Apps Script,它可以让你使用 JavaScript 做一些非常有用的事情,比如运行代码来响应文档打开事件或单元格更改事件,为公式制作自定义电子表格函数等等。,与运行在「用户电脑中」的客户端应用不同,用户的自定义脚本通常只能影响用户自已,而对于在线的应用或服务来讲,有一些情况就变得更为重要,比如「安全」,用户的「自定义脚本」必须严格受到限制和隔离,即不能影响到宿主程序,也不能影响到其它用户。,另外,有一些牵扯「模板化」的前端框架,如Vue.js、Venom.js等都会用到动态代码执行。,handler.getPrototypeOf()是一个代理方法,当读取代理对象的原型时,该方法就会被调用。语法:,当 getPrototypeOf 方法被调用时,this 指向的是它所属的处理器对象,getPrototypeOf 方法的返回值必须是一个对象或者 null。,在 JavaScript 中,有下面这五种操作(方法/属性/运算符)可以触发 JS 引擎读取一个对象的原型,也就是可以触发 getPrototypeOf() 代理方法的运行:,如果遇到了下面两种情况,JS 引擎会抛出 TypeError[10] 异常:,基本用法:,5 种触发 getPrototypeOf 代理方法的方式:,两种异常的情况:,我们知道在 JavaScript 中的作用域(scope)只有全局作用域(global scope)、函数作用域(function scope)以及从 ES6 开始才有的块级作用域(block scope)。如果要将一段代码中的变量、函数等的定义隔离出来,受限于 JavaScript 对作用域的控制,只能将这段代码封装到一个 Function 中,通过使用 function scope 来达到作用域隔离的目的。也因为需要这种使用函数来达到作用域隔离的目的方式,于是就有 IIFE(立即调用函数表达式),这是一个被称为“自执行匿名函数”的设计模式。,当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问,它拥有独立的词法作用域。不仅避免了外界访问 IIFE 中的变量,而且又不会污染全局作用域,弥补了 JavaScript 在 scope 方面的缺陷。一般常见于写插件和类库时,如 JQuery 当中的沙箱模式,当将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。,模拟原生浏览器对象的目的是为了防止闭包环境,操作原生对象,篡改污染原生环境,完成模拟浏览器对象之前我们需要先关注几个不常用的 API。,eval,eval 函数可将字符串转换为代码执行,并返回一个或多个值:,由于 eval 执行的代码可以访问闭包和全局范围,因此就导致了代码注入的安全问题,因为代码内部可以沿着作用域链往上找,篡改全局变量,这是我们不希望的。,补充几个点:,new Function,Function构造函数创建一个新的 Function 对象。直接调用这个构造函数可用于动态创建函数。,arg1, arg2, ... argN 被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的 JavaScript 标识符的字符串,或者一个用逗号分隔的有效字符串的列表,例如“×”,“theValue”,或“a,b”。,补充几个点:,functionBody,一个含有包括函数定义的 JavaScript 语句的字符串。,同样也会遇到和 eval 类似的的安全问题和相对较小的性能问题。,与 eval 不同的是 Function 创建的函数只能在全局作用域中运行,它无法访问局部闭包变量,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造器创建时所在的作用域的变量。new Function()是 eval()更好替代方案。它具有卓越的性能和安全性,但仍没有解决访问全局的问题。,with,with 是 JavaScript 中一个关键字,扩展一个语句的作用域链。它允许半沙盒执行。那什么叫半沙盒?语句将某个对象添加到作用域链的顶部,如果在沙盒中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出 ReferenceError。,究其原理,with在内部使用in运算符。对于块内的每个变量访问,它都在沙盒条件下计算变量。如果条件是 true,它将从沙盒中检索变量。否则,就在全局范围内查找变量。但是 with 语句使程序在查找变量值时,都是先在指定的对象中查找。所以对于那些本来不是这个对象的属性的变量,查找起来会很慢,对于有性能要求的程序不适合(JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符)。with 也会导致数据泄漏(在非严格模式下,会自动在全局作用域创建一个全局变量)。,in 运算符,in 运算符能够检测左侧操作数是否为右侧操作数的成员。其中,左侧操作数是一个字符串,或者可以转换为字符串的表达式,右侧操作数是一个对象或数组。,with + new Function,配合 with 用法可以稍微限制沙盒作用域,先从当前的 with 提供对象查找,但是如果查找不到依然还能从更上面的作用域获取,污染或篡改全局环境。,可以看到,基于上面的方案都多多少少存在一些安全问题:,那有没有更安全一些的沙箱环境实现呢?,基于 Proxy 实现的沙箱(ProxySandbox),ES6 Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种“元编程”(meta programming)。,我们知道无论 eval 还是 function,执行时都会把作用域一层一层向上查找,如果找不到会一直到 global,那么利用 Proxy 的原理就是,让执行了代码在 sandobx 中找的到,以达到「防逃逸」的目的。,我们前面提到with在内部使用in运算符来计算变量,如果条件是 true,它将从沙盒中检索变量。理想状态下没有问题,但也总有些特例独行的存在,比如 Symbol.unscopables。,Symbol 对象的 Symbol.unscopables 属性,指向一个对象。该对象指定了使用 with 关键字时,哪些属性会被 with 环境排除。,上面代码说明,数组有 6 个属性,会被 with 命令排除。,由此我们的代码还需要修改如下:,Symbol.unscopables 定义对象的不可作用属性。Unscopeable 属性永远不会从 with 语句中的沙箱对象中检索,而是直接从闭包或全局范围中检索。,快照沙箱(SnapshotSandbox),快照沙箱实现来说比较简单,主要用于不支持 Proxy 的低版本浏览器,原理是基于diff来实现的,在子应用激活或者卸载时分别去通过快照的形式记录或还原状态来实现沙箱,snapshotSandbox 会污染全局 window。,我们看下 qiankun[13] 的 snapshotSandbox 的源码,这里为了帮助理解做部分精简及注释。,legacySandBox,qiankun 框架 singular 模式下的 proxy 沙箱实现,为了便于理解,这里做了部分代码的精简和注释。,legacySandBox 还是会操作 window 对象,但是他通过激活沙箱时还原子应用的状态,卸载时还原主应用的状态来实现沙箱隔离,同样会对 window 造成污染,但是性能比快照沙箱好,不用遍历 window 对象。,proxySandbox(多例沙箱),在 qiankun 的沙箱 proxySandbox 源码里面是对 fakeWindow 这个对象进行了代理,而这个对象是通过 createFakeWindow 方法得到的,这个方法是将 window 的 document、location、top、window 等等属性拷贝一份,给到 fakeWindow。,源码展示:,proxySandbox 由于是拷贝复制了一份 fakeWindow,不会污染全局 window,同时支持多个子应用同时加载。,详细源码请查看:proxySandbox[14]。,VM,VM是 Node.js 默认提供的一个内建模块,VM 模块提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。JavaScript 代码可以被编译并立即运行,或编译、保存然后再运行。,执行上这的代码就能拿到结果 3,同时,通过 vm.Script 还能指定代码执行的「最大毫秒数」,超过指定的时长将终止执行并抛出一个异常:,上面的脚本执行将会失败,被检测到超时并抛出异常,然后被 Try Cache 捕获到并打出 log,但同时需要注意的是 vm.Script 的 timeout 选项「只针对同步代有效」,而不包括是异步调用的时间,比如:,上述代码,并不是会在 50ms 后抛出异常,因为 50ms 上边的代码同步执行肯定完了,而 setTimeout 所用的时间并不算在内,也就是说 vm 模块没有办法对异步代码直接限制执行时间。我们也不能额外通过一个 timer 去检查超时,因为检查了执行中的 vm 也没有方法去中止掉。,另外,在 Node.js 通过 vm.runInContext 看起来似乎隔离了代码执行环境,但实际上却很容易「逃逸」出去。我们看下这个过程。,使用VM模块我们可以在独立的环境中运行不受信任的代码,这就意味着运行在沙箱里的代码不能访问Node进程了,对吗?,基本的使用示例代码:,现在我们尝试访问进程:,“process is not defined”,所以默认情况下VM模块不能访问主进程,如果想要访问需要指定授权。,看起来默认不能访问“process、require”等就满足需求了,但是真的就没有办法触及主进程并执行代码了?,看下面的例子:,在javascript中this指向它所属的对象,所以我们使用它时就已经指向了一个VM上下文之外的对象。那么访问this的.constructor 就返回 Object Constructor ,访问 Object Constructor 的 .constructor 返回 Function constructor 。,Function constructor 就像javascript提供的最高函数,他可以访问全局,所以他能返回全局事物。Function constructor允许从字符串生成函数,从而执行任意代码。,所以我们可以使用 Function constructor 返回主进程。关于 Function constructor 更多内容在这里[15]和这里[16]。,可以正常打印,也就是说顺利拿到了主进程的process,也就是上面所说的产生了「逃逸」。这招同样对突破Angular同样有效 —— AngularJS 沙箱[17]。,再看下面的例子:,执行上边的代码,宿主程序立即就会「退出」,sandbox 是在 VM 之外的环境创建的,需 VM 中的代码的 this 指向的也是 sandbox,那么:,没有人愿意用户一段脚本就能让应用挂掉吧。除了退出进程序之外,实际上还能干更多的事情。,有个简单的方法就能避免通过 this.constructor 拿到 process,如下:,但还是有风险的,由于 JavaScript 本身的动态的特点,各种黑魔法防不胜防。事实 Node.js 的官方文档中也提到「 不要把 VM 当做一个安全的沙箱,去执行任意非信任的代码」。,VM2,在社区中有一些开源的模块用于运行不信任代码,例如 sandbox、vm2、jailed 等。相比较而言 vm2 对各方面做了更多的安全工作,相对安全些。「这也是为什么imageCook采用了该沙箱模块」,从 vm2 的官方 README 中可以看到,它基于 Node.js 内建的 VM 模块,来建立基础的沙箱环境,然后同时使用上了文介绍过的 ES6 的 Proxy 技术来防止沙箱脚本逃逸。,用同样的测试代码来试试 vm2:,如上代码,并没有成功结束掉宿主程序,vm2 官方 REAME 中说「vm2 是一个沙盒,可以在 Node.js 中安全的执行不受信任的代码」。,然而,事实上我们还是可以干一些「坏」事情,比如:,只要能干坏事情,就是不安全的,上边的代码将永远不会执行结束,如同 Node.js 内建模块一样,vm2 的 timeout 对异步操作是无效的。同时,vm2 也不能额外通过一个 timer 去检查超时,因为它也没有办法将执行中的 vm 终止掉。这会一点点耗费完服务器的资源,让你的应用挂掉。,那么或许你会想,我们能不能在上边的 sandbox 中放一个假的 Promise 从而禁掉 Promise 呢?答案是能提供一个「假」的 Promise,但却没有办法完成禁掉 Promise,比如:,可以看到通过一行 Promise = (async function(){})().constructor 就可以轻松再次拿到 Promise 了。从另一个层面来看,况且或许有时我们还想让自定义脚本支持异步处理呢。,关于VM2还有更多新的和创新性的绕过 ——更多逃逸[18]。,除了从沙箱逃逸,还可以使用 infinite while loop 创建无限循环拒绝服务。,Safeify[19]:Node.js环境下建立一个更安全的沙箱,通过上文的探究,我们并没有找到一个完美的方案在 Node.js 建立安全的隔离的沙箱。其中 vm2 做了不少处理,相对来讲算是较安全的方案了,但问题也很明显,比如异步不能检查超时的问题以及和宿主程序在相同进程的问题。,没有进程隔离时,通过 VM 创建的 sanbox 大体是这样的,那么,我们是不是可以尝试,将非受信代码,通过 vm2 这个模块隔离在一个独立的进程中执行呢?然后,执行超时时,直接将隔离的进程干掉,但这里我们需要考虑如下几个问题:,如果来一个执行任务,创建一个进程,用完销毁,仅处理进程的开销就已经稍大了,并且也不能不设限的开新进程和宿主应用抢资源,那么,需要建一个进程池:,前提:所有任务到来会创建一个 Script 实例,先进入一个 pending 队列,然后直接将 script 实例的 defer 对象返回,调用处就能 await 执行结果了。,然后:由 sandbox master 根据工程进程的空闲程序来调度执行,master 会将 script 的执行信息,包括重要的 ScriptId,发送给空闲的 worker,worker 执行完成后会将「结果 + script 信息」回传给 master,master 通过 ScriptId 识别是哪个脚本执行完毕了,就是结果进行 resolve 或 reject 处理。,这样,通过「进程池」既能降低「进程来回创建和销毁的开销」,也能确保不过度抢占宿主资源;同时,在异步操作超时,还能将工程进程直接杀掉;同时,master 将发现一个工程进程挂掉,会立即创建替补进程。,进程间如何通讯,需要「动态代码」操作数据后可以直接序列化然后通过 IPC 发送给隔离 Sandbox 进程,执行结果一样经过序列化通过 IPC 传输。,其中,如果想公开一个方法给 sandbox,因为不在一个进程,并不能方便的将一个方案的引用传递给 sandbox。我们可以将宿主的方法,在传递给 sandbox worker 之类做一下处理,转换为一个「描述对象」,包括了允许 sandbox 调用的方法信息,然后将信息,如同其它数据一样发送给 worker 进程,worker 收到数据后,识别出「方法描述对象」,然后在 worker 进程中的 sandbox 对象上建立代理方法,代理方法同样通过 IPC 和 master 通讯。,在 Linux 平台,通过 CGroups 对沙箱进程进行整体的 CPU 和内存等资源的配额限制,CGroups 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组(Process Groups)所使用的物理资源(如:CPU、Memory、IO 等等)的机制。最初由 Google 的工程师提出,后来被整合进 Linux 内核。CGroups 也是 LXC 为实现虚拟化所使用的资源管理手段,可以说没有 CGroups 就没有 LXC。,最终,我们建立了一个大约这样的「沙箱环境」。,如此这般处理起来是不是感觉很麻烦?但我们就有了一个更加安全一些的沙箱环境了,基于这些处理被封装为一个独立的模块 Safeify[20],在Github上已经开源。,相较于内建的 VM 及常见的几个沙箱模块, Safeify 具有如下特点:,简单介绍一下 Safeify 如何使用,通过如下命令安装:,在应用中使用,还是比较简单的,如下代码(TypeScript 中类似)。,关于安全的问题,没有最安全,只有更安全。Safeify 已在部分项目中使用,但自定义脚本的功能是往往仅针对内网用户,有不少动态执行代码的场景其实是可以避免的,绕不开或实在需要提供这个功能时,希望本文或 Safeify 能对大家有所帮助就行了。,运行不信任的代码是非常困难的,只依赖软件模块作为沙箱技术,防止不受信任代码用于非正当用途是不得已的决定。这可能促使云上SAAS应用的不安全,因为通过逃逸出沙箱进程多个租户间的数据可能被访问(主进程数据获取),这样你就可能可以通过session,secret等来潜入其他租户。一个更安全的选择是依赖于硬件虚拟化,比如每个租户代码在独立的docker容器或AWS Lambada Function 中执行会是更好的选择。,下面是Auth0如何处理沙箱问题:Sandboxing Node.js with CoreOS and Docker[21]。「下来可以再详细研究下实现」。,imageCook的使用case。,目标:拿到用于前端页面渲染的index.js + index.css。,基本思路:,[1] 沙箱(Sandbox): http://www.arkteam.net/?p=2967,[2] JavaScript中constructor属性: https://segmentfault.com/a/1190000013245739,[3] class: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class,[4] Class构造方法: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes/constructor,[5] Object.getPrototypeOf(): https://www.axihe.com/api/js-es/ob-object/get-prototype-of.html,[6] Reflect.getPrototypeOf(): https://www.axihe.com/api/js-es/ob-reflect/get-prototype-of.html,[7] proto: https://www.axihe.com/api/js-es/ob-object/proto.html,[8] Object.prototype.isPrototypeOf(): https://www.axihe.com/api/js-es/ob-object/is-prototype-of.html,[9] instanceof: https://www.axihe.com/api/js-es/ex-relational/instanceof.html,[10] TypeError: https://www.axihe.com/api/js-es/ob-error/type-error.html,[11] 专访 Wind.js 作者老赵(上):缘由、思路及发展: https://www.infoq.cn/article/interview-jscex-author-part-1,[12] (0, eval)(‘this’): https://www.cnblogs.com/qianlegeqian/p/3950044.html,[13] qiankun: https://qiankun.umijs.org/zh/guide,[14] :proxySandbox: https://link.segmentfault.com/?enc=Mb%2BNNJjUrmTA7g2uf%2FTgzQ%3D%3D.IHwAeHwf8%2FPDd3WJLo%2F4dCWf2md2lzw7s%2BIEdUcUHmX7xMSccEguXX%2BFQBtpgU8SHiyqxgnCi00SvzmT95eNTRD1XaOHjO5xokQrsy%2BHYtQ%3D,[15] 这里: https://link.juejin.cn/?target=http%3A%2F%2Fdfkaye.github.io%2F2014%2F03%2F14%2Fjavascript-eval-and-function-constructor%2F,[16] 这里: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fi0natan%2Fnodebestpractices%2Fissues%2F211,[17] AngularJS 沙箱: https://link.juejin.cn/?target=https%3A%2F%2Fportswigger.net%2Fresearch%2Fdom-based-angularjs-sandbox-escapes,[18] 更多逃逸: https://github.com/patriksimek/vm2/issues?q=is%3Aissue+author%3AXmiliaH+is%3Aclosed,[19] Safeify: https://github.com/Houfeng/safeify,[20] Safeify: https://github.com/Houfeng/safeify,[21] Sandboxing Node.js with CoreOS and Docker: https://link.juejin.cn/?target=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Du81pS05W1JY

© 版权声明

相关文章