浅析JavaScript的沙箱内容

开发 前端
市面上现在流行两种沙箱模式,一种是使用iframe,还有一种是直接在页面上使用new Function + eval进行执行. 殊途同归,主要还是防止一些Hacker们 吃饱了没事干,收别人钱来 Hack 你的网站. 一般情况, 我们的代码量有60%业务+40%安全. 剩下的就看天意了.接下来,我们来一步一步分析,如果做到在前端的沙箱.文末 看俺有没有心情放一个彩蛋吧.

[[171101]]

市面上现在流行两种沙箱模式,一种是使用iframe,还有一种是直接在页面上使用new Function + eval进行执行. 殊途同归,主要还是防止一些Hacker们 吃饱了没事干,收别人钱来 Hack 你的网站. 一般情况, 我们的代码量有60%业务+40%安全. 剩下的就看天意了.

接下来,我们来一步一步分析,如果做到在前端的沙箱.文末 看俺有没有心情放一个彩蛋吧.

直接嵌套

这种方式说起来并不是什么特别好的点子,因为需要花费比较多的精力在安全性上.

eval执行

最简单的方式,就是使用eval进行代码的执行

eval('console.log("a simple script");'); 
  • 1.

但,如果你是直接这么使用的话, 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"  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

那有没有什么办法可以解决eval这个特性呢?

答: 没有. 除非你不用

ok,那我就不用. 我们这里就可以使用new Function(..args,bodyStr) 来代替eval.

new Function

new Function就是用来,放回一个function obj的. 用法参考:new Function.

所以,上面的代码,放在new Function中,可以写为:

new Function('console.log("a simple script");')(); 
  • 1.

这样做在安全性上和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 
 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

所以,第一步改写上面的new Function(),将里面变量的获取途径控制在自己的手里.

function compileCode (src) {   
  src = 'with (sandbox) {' + src + '}' 
  return new Function('sandbox', src) 
 
  • 1.
  • 2.
  • 3.
  • 4.

这样,所有的内容多会从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);  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

// 相当于检查 获取的变量是否在里面 like: 'in'

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

这样的话,就能完美的解决掉 向上查找变量的烦恼了. 另外一些大神,发现在新的ECMA里面,有些方法是不会被with scope 影响的. 这里,主要是通过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
 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

现在,基本上就可以宣告你的代码是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) 
        } 
    })() 
 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

不过上面,这样的缓存机制有个弊端,就是不能存储多个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)) 
    } 
 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

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

接着,我们来看一下,如果使用iframe,来实现代码的编译. 这里,Jsfiddle就是使用这种办法.

iframe 嵌套

最简单的方式就是,使用sandbox属性. 该属性可以说是真正的沙盒... 把sandbox加载iframe里面,那么,你这个iframe基本上就是个标签而已... 而且支持性也挺棒的,比如IE10.

<iframe sandbox src=”...”></iframe> 
  • 1.

这样已添加,那么下面的事,你都不可以做了:

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> 
  • 1.

常用的配置项有:

配置 效果
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> 
  • 1.

这样,就可以保证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>  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

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

postMessage 讲解

postMessage主要做的事情有三个:

1.页面和其打开的新窗口的数据传递

2.多窗口之间消息传递

3.页面与嵌套的iframe消息传递

具体的格式为 

otherWindow.postMessage(message, targetOrigin, [transfer]); 
  • 1.

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
 
  // ... 
 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

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>  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

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); 
    });  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

实际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);  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

在vm构建出来的sandbox里面,没有任何可以访问的全局变量.除了基本的syntax.

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2021-07-27 22:56:00

JavaScript编程开发

2016-10-19 14:35:20

JavaScript函数式编程

2010-09-28 14:12:50

Javascript

2021-10-25 10:30:12

JavaScript开发 代码

2011-03-07 09:41:10

JavaScript

2009-07-24 17:30:37

Javascript闭

2021-02-07 22:59:55

JavaScript编程方法链

2021-04-09 08:51:32

Web WorkerJavaScript微前端

2011-03-10 14:19:56

JavaScript

2009-07-15 16:03:26

Swing线程

2011-03-08 09:15:04

JavaScript

2022-01-17 21:37:24

JavaScriptHTMLCSS

2023-11-12 21:37:56

Feed流

2016-09-14 21:28:25

JavaScript事件代理委托

2009-07-14 11:34:42

MyEclipse断点JavaScript

2009-09-16 16:32:20

JavaScript静

2021-03-16 10:00:40

JavaScript用户登录表单JavaScript基

2021-10-19 09:31:19

Javascript 登录表单前端

2024-12-26 16:13:53

JavaScript开发表单

2010-09-30 15:19:33

点赞
收藏

51CTO技术栈公众号