在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言: 说到沙箱,我们的脑海中可能会条件反射地联想到上面这个画面并瞬间变得兴致满满,不过很可惜本文并不涉及“我的世界”(老封面党了),下文将逐步介绍“浏览器世界”的沙箱。 1、什么是沙箱在计算机安全中, 沙箱( 例如,下列场景就涉及了沙箱这一抽象的概念:
2、沙箱有什么应用场景上述介绍了一些较为宏观的沙箱场景,其实在日常的开发中也存在很多的场景需要应用这样一个机制:
总而言之,只要遇到不可信的第三方代码,我们就可以使用沙箱将代码进行隔离,从而保障外部程序的稳定运行。如果不做任何处理地执行不可信代码,在前端中最直观的副作用/危害就是污染、篡改全局 // 子应用代码 window.location.href = 'www.diaoyu.com' Object.prototype.toString = () => { console.log('You are a fool :)') } document.querySelectorAll('div').forEach(node => node.classList.add('hhh')) sendRequest(document.cookie) ... 3、如何实现一个 JS 沙箱要实现一个沙箱,其实就是去制定一套程序执行机制,在这套机制的作用下 沙箱内部程序的运行不会影响到外部程序的运行 。 3.1 最简陋的沙箱要实现这样一个效果,最直接的想法就是程序中访问的 所有变量均来自可靠或自主实现的上下文环境而不会从全局的执行环境中取值, 那么要实现变量的访问均来自一个可靠上下文环境, 我们需要为待执行程序构造一个作用域: // 执行上下文对象 const ctx = func: variable => { console.log(variable) }, foo: 'foo' } // 最简陋的沙箱 function poorestSandbox(code, ctx) { eval(code) // 为执行程序构造了一个函数作用域 } // 待执行程序 const code = ` ctx.foo = 'bar' ctx.func(ctx.foo) ` poorestSandbox(code, ctx) // bar 这样的一个沙箱要求源程序在获取任意变量时都要加上执行上下文对象的前缀,这显然是非常不合理的,因为我们没有办法控制第三方的行为,是否有办法去掉这个前缀呢? 3.2 非常简陋的沙箱(With)使用 // 执行上下文对象 const ctx = { func: variable => { console.log(variable) }, foo: 'foo' } // 非常简陋的沙箱 function veryPoorSandbox(code, ctx) { with(ctx) { // Add with eval(code) } } // 待执行程序 const code = ` foo = 'bar' func(foo) ` veryPoorSandbox(code, ctx) // bar 这样一来就 实现了执行程序中的变量在沙箱提供的上下文环境中查找先于外部执行环境 的效果。 问题来了,在提供的上下文对象中没有找到某个变量时,代码仍会沿着作用域链一层一层向上查找,这样的一个沙箱仍然无法控制内部代码的执行。我们 希望沙箱中的代码只在手动提供的上下文对象中查找变量,如果上下文对象中不存在该变量则直接报错或返回 3.3 没那么简陋的沙箱(With + Proxy)为了解决上述抛出的问题,我们借助
由于 has 会拦截 // 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例 function withedYourCode(code) { code = 'with(globalObj) {' + code + '}' return new Function('globalObj', code) } // 可访问全局作用域的白名单列表 const access_white_list = ['Math', 'Date'] // 待执行程序 const code = ` Math.random() location.href = 'xxx' func(foo) ` // 执行上下文对象 const ctx = { func: variable => { console.log(variable) }, foo: 'foo' } // 执行上下文对象的代理对象 const ctxProxy = new Proxy(ctx, { has: (target, prop) => { // has 可以拦截 with 代码块中任意属性的访问 if (access_white_list.includes(prop)) { // 在可访问的白名单内,可继续向上查找 return target.hasOwnProperty(prop) } if (!target.hasOwnProperty(prop)) { throw new Error(`Invalid expression - ${prop}! You can not do that!`) } return true } }) // 没那么简陋的沙箱 function littlePoorSandbox(code, ctx) { withedYourCode(code).call(ctx, ctx) // 将 this 指向手动构造的全局代理对象 } littlePoorSandbox(code, ctxProxy) // Uncaught Error: Invalid expression - location! You can not do that! 到这一步,其实很多较为简单的场景就可以覆盖了( 从而又衍生出另一个问题——如何让子程序使用所有全局对象的同时不影响外部的全局状态呢? 3.4 天然的优质沙箱(iframe)听到上面这个问题 试想一个这样的场景:一个页面中有多个沙箱窗口,其中有一个沙箱需要与主页面共享几个全局状态(eg: 点击浏览器回退按钮时子应用也会跟随着回到上一级),另一个沙箱需要与主页面共享另外一些全局状态(eg: 共享 cookie 登录态)。 虽然浏览器为主页面和 3.5应该能用的沙箱(With + Proxy + iframe)为了实现上述场景,我们把上述方法缝合一下即可:
// 沙箱全局代理对象类 class SandboxGlobalProxy { constructor(sharedState) { // 创建一个 iframe 对象,取出其中的原生浏览器全局对象作为沙箱的全局对象 const iframe = document.createElement('iframe', {url: 'about:blank'}) document.body.appendChild(iframe) const sandboxGlobal = iframe.contentWindow // 沙箱运行时的全局对象 return new Proxy(sandboxGlobal, { has: (target, prop) => { // has 可以拦截 with 代码块中任意属性的访问 if (sharedState.includes(prop)) { // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找 return false } if (!target.hasOwnProperty(prop)) { throw new Error(`Invalid expression - ${prop}! You can not do that!`) } return true } }) } } function maybeAvailableSandbox(code, ctx) { withedYourCode(code).call(ctx, ctx) } const code_1 = ` console.log(history == window.history) // false window.abc = 'sandbox' Object.prototype.toString = () => { console.log('Traped!') } console.log(window.abc) // sandbox ` const sharedGlobal_1 = ['history'] // 希望与外部执行环境共享的全局对象 const globalProxy_1 = new SandboxGlobalProxy(sharedGlobal_1) maybeAvailableSandbox(code_1, globalProxy_1) window.abc // undefined Object.prototype.toString() // [object Object] 并没有打印 Traped 从实例代码的结果可以看到借用 3.6 沙箱逃逸(Sandbox Escape)沙箱于作者而言是一种安全策略,但于使用者而言可能是一种束缚。脑洞大开的开发者们尝试用各种方式摆脱这种束缚,也称之为 沙箱逃逸 。因此一个沙箱程序最大的挑战就是如何检测并禁止这些预期之外的程序执行。 上面实现的沙箱似乎已经满足了我们的功能,大功告成了吗?其实不然,下列操作均会对沙箱之外的环境造成影响,实现沙箱逃逸: 访问沙箱执行上下文中某个对象内部属性时, // 访问沙箱对象中对象的属性时,省略了上文中的部分代码 const ctx = { window: { parent: {...}, ... } } const code = ` window.parent.abc = 'xxx' ` window.abc // xxx
const code = ` ({}).constructor.prototype.toString = () => { console.log('Escape!') } ` ({}).toString() // Escape! 预期是 [object Object] 3.7 “无瑕疵”的沙箱(Customize Interpreter)
|
请发表评论