JS构建沙箱环境sandbox
市⾯上现在流⾏两种沙箱模式,⼀种是使⽤iframe,还有⼀种是直接在页⾯上使⽤new Function + eval进⾏执⾏。 殊途同归,主要还是防⽌⼀些Hacker们 吃饱了没事⼲,收别⼈钱来 Hack 你的⽹站。 ⼀般情况, 我们的代码量有60%业务+40%安全. 剩下的就看天意了。接下来,我们来⼀步⼀步分析,如果做到在前端的沙箱.⽂末 看俺有没有⼼情放⼀个彩蛋吧。
直接嵌套
这种⽅式说起来并不是什么特别好的点⼦,因为需要花费⽐较多的精⼒在安全性上.
eval执⾏
最简单的⽅式,就是使⽤eval进⾏代码的执⾏ eval('console.log("a simple script");');
但,如果你是直接这么使⽤的话, 因为,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('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特性,说起来也是蛮⽜逼的. 可以将获取对象上的所有⽅式改写.具体⽤法可以参考: . 这⾥,我们只要将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 + '}'
iframe嵌套页面加载慢
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 (sandbox))
}
}
差不多了, 如果不嫌写的丑,可以直接拿去⽤.(如果出事,纯属巧合,本⼈概不负责).
接着,我们来看⼀下,如果使⽤iframe,来实现代码的编译. 这⾥,就是使⽤这种办法.
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,storageallow-top-
配置效果
navigation允许iframe能够主导p进⾏页⾯跳转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。 更多详细的内容,请参考:
接下来,我们来具体讲解,如果使⽤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) {
// 相当于p.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 = igin || igin; // For Chrome, the origin property is in iginalEvent object.
if (origin !== ":8080")
return;
// ...
}
event⾥⾯,会带上3个参数:
data: 传递过来的数据. e.data
origin: 发送信息的URL, ⽐如:
source: 发送信息的源页⾯的window对象. 我们实际上只能从上⾯获取信息.
该API常常⽤在window和iframe的信息交流当中. 现在,我们回到上⾯的内容.
<!-- frame.html -->
<!DOCTYPE html>
<html>
<head>
<title>Evalbox's Frame</title>
<script>
window.addEventListener('message', function (e) {
// 相当于p.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写⼊:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。