动手写 js 沙箱

本文由云+社区宣布

作者:ivweb villainthr

市面上如今盛行两种沙箱形式,一种是运用iframe,另有一种是直接在页面上运用new Function + eval举行实行。 异曲同工,重要照样防备一些Hacker们 吃饱了没事干,收他人钱来 Hack 你的网站。 平常状况, 我们的代码量有60%营业+40%平安. 剩下的就看天意了。接下来,我们来一步一步剖析,假如做到在前端的沙箱.文末 看俺有无心境放一个彩蛋吧。

直接嵌套

这类体式格局说起来并不是什么迥殊好的点子,由于须要消费比较多的精神在平安性上.

eval实行

最简朴的体式格局,就是运用eval举行代码的实行 eval('console.log("a simple script");');

但,假如你是直接这么运用的话, congraduations… do die… 由于,eval 的特征是假如当前域内里没有,则会向上遍历.一直到最顶层的global scope 比方window.以及,他还可以接见closure内的变量.看demo:

function Auth(username)
{
  var password = "trustno1";
  this.eval = function(name) { return eval(name) } // 相当于直接this.name
}

auth = new Auth("Mulder")
console.log(auth.eval("username")); // will print "Mulder"
console.log(auth.eval("password")); // will print "trustno1"

那有无什么要领可以处理eval这个特征呢? 答: 没有. 除非你不必 ok,那我就不必. 我们这里就可以够运用new Function(..args,bodyStr) 来替代eval。

new Function

new Function就是用来,放回一个function obj的. 用法参考:new Function. 所以,上面的代码,放在new Function中,可以写为: new Function('console.log("a simple script");')();

如许做在平安性上和eval没有多大的差异,不过,他不能接见closure的变量,即经由过程this来挪用,而且他的机能比eval要好许多. 那有无要领处理global var的要领呢? 有啊… 只是有点庞杂先用with,在用Proxy

with

with这个特征,也算是一个比较鸡肋的,他和eval并列为js两大SB特征. 不说无用, bug还多,平安性就没谁了… 然则, with的套路老是有人喜好的.在这里,我们就须要运用到他的特征.由于,在with的scope内里,一切的变量都邑先从with定义的Obj上查找一遍。

 var a = {
    c:1
}
var c =2;
with(a){
    console.log(c); //等价于c.a
}

所以,第一步改写上面的new Function(),将内里变量的猎取门路控制在本身的手里。

 function compileCode (src) {  
  src = 'with (sandbox) {' + src + '}'
  return new Function('sandbox', src)
}

如许,一切的内容多会从sandbox这个str上面猎取,然则找不到的var则又会向上举行搜刮. 为了处理这个题目,则须要运用: proxy

proxy

es6 供应的Proxy特征,说起来也是蛮牛逼的. 可以将猎取对象上的一切体式格局改写.细致用法可以参考: 超好用的proxy. 这里,我们只要将has给换掉即可. 有的就好,没有的就返回undefined

function compileCode (src) {
  src = 'with (sandbox) {' + src + '}'
  const code = new Function('sandbox', src)

  return function (sandbox) {
    const sandboxProxy = new Proxy(sandbox, {has})
    return code(sandboxProxy)
  }
}

// 相当于搜检 猎取的变量是不是在内里 like: 'in'
function has (target, key) {
  return true
}

compileCode('log(name)')(console);

如许的话,就可以圆满的处理掉 向上查找变量的懊恼了。 别的一些,大神,发如今新的ECMA内里,有些要领是不会被with scope 影响的. 这里,重如果经由过程Symbol.unscopables 这个特征来检测的.比方:

 Object.keys(Array.prototype[Symbol.unscopables]); 
// ["copyWithin", "entries", "fill", "find", "findIndex", 
//  "includes", "keys", "values"]

不过,经由本人测试发明也只要Array.prototype上面带有这个属性… 为难… 所以,平常而言,我们可以加上 Symbol.unscopables, 也可以不加。

 // 照样加一下吧
function compileCode (src) {  
  src = 'with (sandbox) {' + src + '}'
  const code = new Function('sandbox', src)

  return function (sandbox) {
    const sandboxProxy = new Proxy(sandbox, {has, get})
    return code(sandboxProxy)
  }
}

function has (target, key) {  
  return true
}

function get (target, key) {  
// 如许,接见Array内里的 like, includes之类的要领,就可以够保证平安... 算了,就当我没说,真的没啥用...
  if (key === Symbol.unscopables) return undefined
  return target[key]
}

如今,基础上就可以够宣布你的代码是99.999% 的5位平安数.(横竖不是100%就行)

设置缓存

假如上代码,每次编译一次code时,都邑实例一次Proxy, 如许做会比较损机能. 所以,我们这里,可以运用closure来举行缓存。 上面天生proxy代码,改写为:

 function compileCode(src) {
    src = 'with (sandbox) {' + src + '}'
    const code = new Function('sandbox', src)

    function has(target, key) {
        return true
    }

    function get(target, key) {
        if (key === Symbol.unscopables) return undefined
        return target[key]
    }

    return (function() {
        var _sandbox, sandboxProxy;
        return function(sandbox) {
            if (sandbox !== _sandbox) {
                _sandbox = sandbox;
                sandboxProxy = new Proxy(sandbox, { has, get })
            }
            return code(sandboxProxy)
        }
    })()
}

不过上面,如许的缓存机制有个弊病,就是不能存储多个proxy. 不过,你可以运用Array来处理,或许更好的运用Map. 这里,我们两个都不必,用WeakMap来处理这个problem. WeakMap 重要的题目在于,他可以圆满的完成,内部变量和外部的内容的一致. WeakMap最大的特性在于,他存储的值是不会被渣滓接纳机制关注的. 说白了, WeakMap援用变量的次数是不会算在援用渣滓接纳机制里, 而且, 假如WeakMap存储的值在外部被渣滓接纳装配接纳了,WeakMap内里的值,也会被删除–同步结果.所以,毫无不测, WeakMap是我们最好的一个tricky. 则,代码可以写为:

const sandboxProxies = new WeakMap()
function compileCode(src) {
    src = 'with (sandbox) {' + src + '}'
    const code = new Function('sandbox', src)

    function has(target, key) {
        return true
    }

    function get(target, key) {
        if (key === Symbol.unscopables) return undefined
        return target[key]
    }
    return function(sandbox) {
        if (!sandboxProxies.has(sandbox)) {
            const sandboxProxy = new Proxy(sandbox, { has, get })
            sandboxProxies.set(sandbox, sandboxProxy)
        }
        return code(sandboxProxies.get(sandbox))
    }
}

差不多了, 假如不嫌写的丑,可以直接拿去用.(假如失事,纯属巧合,本人概不负责).

接着,我们来看一下,假如运用iframe,来完成代码的编译. 这里,Jsfiddle就是运用这类要领.

iframe 嵌套

最简朴的体式格局就是,运用sandbox属性. 该属机可以说是真正的沙盒… 把sandbox加载iframe内里,那末,你这个iframe基础上就是个标签罢了… 而且支撑性也挺棒的,比方IE10. <iframe sandbox src=”...”></iframe>

如许已增加,那末下面的事,你都不可以做了:

1. script剧本不能实行
2. 不能发送ajax要求
3. 不能运用当地存储,即localStorage,cookie等
4. 不能建立新的弹窗和window, 比方window.open or target="_blank"
5. 不能发送表单
6. 不能加载分外插件比方flash等
7. 不能实行自动播放的tricky. 比方: autofocused, autoplay

看到这里,我也是醉了。 好好的一个iframe,你如许是不是是有点过分了。 不过,你可以放宽一点权限。在sandbox内里举行一些简朴设置 <iframe sandbox=”allow-same-origin” src=”...”></iframe>

经常使用的设置项有:

设置结果
allow-forms许可举行提交表单
allow-scripts运转实行剧本
allow-same-origin许可同域要求,比方ajax,storage
allow-top-navigation许可iframe可以主导window.top举行页面跳转
allow-popups许可iframe中弹出新窗口,比方,window.open,target=”_blank”
allow-pointer-lock在iframe中可以锁定鼠标,重要和鼠标锁定有关

可以经由过程在sandbox里,增加许可举行的权限. <iframe sandbox=”allow-forms allow-same-origin allow-scripts” src=”...”></iframe>

如许,就可以够保证js剧本的实行,然则制止iframe里的javascript实行top.location = self.location。 更多细致的内容,请参考: please call me HR.

接下来,我们来细致解说,假如运用iframe来code evaluation. 内里的道理,照样用到了eval.

iframe 剧本实行

上面说到,我们须要运用eval举行要领的实行,所以,须要在iframe上面增加上, allow-scripts的属性.(固然,你也可以运用new Function, 这个随你…) 这里的框架是运用postMessage+eval. 一个用来通讯,一个用来实行. 先看代码:

<!-- frame.html -->
<!DOCTYPE html>
<html>
 <head>
   <title>Evalbox's Frame</title>
   <script>
     window.addEventListener('message', function (e) {
     // 相当于window.top.currentWindow.
       var mainWindow= e.source;
       var result = '';
       try {
         result = eval(e.data);
       } catch (e) {
         result = 'eval() threw an exception.';
       }
       // e.origin 就是本来window的url
       mainWindow.postMessage(result, e.origin);
     });
   </script>
 </head>
</html>

这里趁便插播一下关于postMessage的相干知识点.

postMessage 解说

postMessage重要做的事变有三个:

1.页面和其翻开的新窗口的数据通报

2.多窗口之间音讯通报

3.页面与嵌套的iframe音讯通报

细致的花样为: otherWindow.postMessage(message, targetOrigin, [transfer]);

message是通报的信息,targetOrigin指定的窗口内容,transfer取值为Boolean 示意是不是可以用来对obj举行序列化,相当于JSON.stringify, 不过平常状况下传obj时,会本身先运用JSON举行seq一遍. 细致说一下targetOrigin. targetOrigin的写入花样平常为URI,即, protocol+host. 别的,也可以写为*. 用来示意 传到恣意的标签页中. 别的,就是接收端的参数.接收通报的信息,平常是运用window监听message事宜.

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event)
{
  var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
  if (origin !== "http://example.org:8080")
    return;

  // ...
}

event内里,会带上3个参数:

  • data: 通报过来的数据. e.data
  • origin: 发送信息的URL, 比方: https://example.org
  • source: 发送信息的源页面的window对象. 我们现实上只能从上面猎取信息.

该API常经常使用在window和iframe的信息交换当中. 如今,我们回到上面的内容.

<!-- frame.html -->
<!DOCTYPE html>
<html>
 <head>
   <title>Evalbox's Frame</title>
   <script>
     window.addEventListener('message', function (e) {
     // 相当于window.top.currentWindow.
       var mainWindow= e.source;
       var result = '';
       try {
         result = eval(e.data);
       } catch (e) {
         result = 'eval() threw an exception.';
       }
       // e.origin 就是本来window的url
       mainWindow.postMessage(result, e.origin);
     });
   </script>
 </head>
</html>

iframe内里,已做好文档的监听,然后,我们如今须要举行内容的发送.直接在index.html写入:

// html部份
<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
// 设置基础的平安特征
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

// js部份
function evaluate() {
  var frame = document.getElementById('sandboxed');
  var code = document.getElementById('code').value;
  frame.contentWindow.postMessage(code, '/'); // 只想同源的标签页发送
}

document.getElementById('safe').addEventListener('click', evaluate);

// 同时设置接收部份
window.addEventListener('message',
    function (e) {
      var frame = document.getElementById('sandboxed');
      // 举行信息泉源的考证
      if (e.origin === "null" && e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

现实demo可以参考:H5 ROCK

经常使用的两种沙箱形式这里差不多解说完了. 开首说了文末有个彩蛋,这个彩蛋就是运用nodeJS来做一下沙箱. 比方像 牛客网的代码考证,就是放在后端去做代码的沙箱考证.

彩蛋–nodeJS沙箱

运用nodeJS的沙箱很简朴,就是运用nodeJS供应的VM Module即可. 直接看代码吧:

 const vm = require('vm');
const sandbox = { a: 1, b: 1 };
const script= new vm.Script('a + b');
const context = new vm.createContext(sandbox);
script.runInContext(context);

在vm构建出来的sandbox内里,没有任何可以接见的全局变量.除了基础的syntax.

原文链接:
http://www.ivweb.io/topic/58d…

此文已由腾讯云+社区在各渠道宣布

猎取更多新颖手艺干货,可以关注我们腾讯云手艺社区-云加社区官方号及知乎机构号

    原文作者:腾讯云加社区
    原文地址: https://segmentfault.com/a/1190000018425747
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞