动手写 js 沙箱

时间:2023-02-08 16:15:14

本文由云+社区发表

作者: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/58dba4aedb35a9135d42f845

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

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

动手写 js 沙箱的更多相关文章

  1. 自己动手写js分享插件 &lbrack;支持https&rsqb; &lpar;可以分享QQ空间,微信,新浪微博。。。&rpar;

    由于百度分享,jiathis 等分享插件在https下均会报错,就萌生了自己动手写一个分享插件的念头,其实实现起来一点都不难,以下代码都已在https网站运行通过,特附上以下代码:还请各位看官不吝赐教 ...

  2. 自己动手写js分享插件 &lbrack;支持https&rsqb; &lpar;QQ空间,微信,新浪微博。。。&rpar;

    转载:https://blog.csdn.net/libin_1/article/details/52424340 废话不多说,传送门:http://download.csdn.net/detail/ ...

  3. 自己动手写js分享插件&lpar;QQ空间,微信,新浪微博。。。&rpar;

    参考博客:http://blog.csdn.net/libin_1/article/details/52424340 下载链接:http://download.csdn.net/detail/come ...

  4. 自己动手写插件底层篇—基于jquery移动插件实现

    序言 本章作为自己动手写插件的第一篇文章,会尽可能的详细描述一些实现的方式和预备知识的讲解,随着知识点积累的一点点深入,可能到了后期讲解也会有所跳跃.所以,希望知识点不是很扎实的读者或者是初学者,不要 ...

  5. 自己动手写 ASP&period;NET MVC 分页 part1

    学习编程也有一年半载了,从来没有自己动手写过东西,都是利用搜索软件找代码,最近偶发感慨,难道真的继续做码农??? 突发奇想是不是该自己动手写点东西,可是算法.逻辑思维都太弱了,只能copy网上的代码, ...

  6. 【原创】自己动手写控件----XSmartNote控件

    一.前面的话 在上一篇博文自己动手写工具----XSmartNote [Beta 3.0]中,用到了若干个自定义控件,其中包含用于显示Note内容的简单的Label扩展控件,用于展示标签内容的labe ...

  7. 【原创】自己动手写工具----XSmartNote &lbrack;Beta 3&period;0&rsqb;

    一.前面的话 在动笔之前,一直很纠结到底要不要继续完成这个工具,因为上次给它码代码还是一年多之前的事情,参考自己动手写工具----XSmartNote [Beta 2.0],这篇博文里,很多园友提出了 ...

  8. 【原创】自己动手写工具----XSmartNote &lbrack;Beta 2&period;0&rsqb;

    一.前面的话 在上一篇自己动手写工具----XSmartNote中,我简单介绍了这个小玩意儿的大致界面和要实现的功能,看了一下园子里的评论,评价褒贬不一,有人说“现在那么多云笔记的工具”,“极简版ev ...

  9. 【原创】自己动手写工具----签到器&lbrack;Beta 2&period;0&rsqb;

    一.前面的话 上一篇中基本实现了简单的签到任务,但是不够灵活.在上一篇自己动手写工具----签到器的结尾中,我设想了几个新增功能来提高工具的灵活程度,下面把新增功能点列出来看看: (1)新增其他的进程 ...

随机推荐

  1. 剑指offer题目1-10

    面试题3:二维数组中的查找 public class Solution { public boolean Find(int [][] array,int target) { boolean isFou ...

  2. iOS开发--TableView详细解释

    -.建立 UITableView  DataTable = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 420)];  [Data ...

  3. iOS 多态的简单思想

    /** * 多态:多种形态,是对于对象而言的,如果没有继承就没有多态 * * 在代码中体现:就是通过定义父类指针指向子类的对象 * * 好处:在函数或者方法的参数中如果传入的是父类指针,那么实现的时候 ...

  4. js中Date对象

    Date常用的几个方法: var oDate=new Date(); oDate.getHours()方法是获取当前的小时 oDate.getMinutes()方法获取当前的分钟 oDate.getS ...

  5. 14&period;3&period;2&period;4 Locking Reads 锁定读

    14.3.2.4 Locking Reads 锁定读 如果你的查询数据,然后插入或者更新相关的数据 在同一个事务, 普通的SELECT 语句不足以给予足够保护. 其他事务可以更新或者删除相同的你要查询 ...

  6. Java添加JDBC

    添加JDBC 1.SQL Server SQL Server2005 下载 sqljdbc_4.0 https://www.microsoft.com/en-us/download/details.a ...

  7. 十二、 Spring Boot 静态资源处理

    spring Boot 默认为我们提供了静态资源处理,使用 WebMvcAutoConfiguration 中的配置各种属性. 建议大家使用Spring Boot的默认配置方式,如果需要特殊处理的再通 ...

  8. python全栈开发-Day11 迭代器、生成器、面向过程编程

    一. 迭代器 一 .迭代的概念 迭代器即迭代的工具,那什么是迭代呢? 迭代是一个重复的过程,每次重复即一次迭代,并且每次迭代的结果都是下一次迭代的初始值 while True: #只是单纯地重复,因而 ...

  9. Linux System Programming --Chapter Nine

    这一章的标题是 "信号" ,所以本文将对信号的各个方面进行介绍,由于Linux中的信号机制远比想象的要复杂,所以,本文不会讲的很全面... 信号机制是进程之间相互传递消息的一种方法 ...

  10. jmeter之ip欺骗

    说明:我看有的博客说官方文档是在jmeter2.5以上的版本有此功能的实现~ 我的是2.13版本,也可以实现 . 准备工作: 使用IP欺骗功能必须得本地有多个可用IP,通常普通的PC机只有一个物理网卡 ...