乐趣区

动手写 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…

此文已由腾讯云 + 社区在各渠道发布
获取更多新鲜技术干货,可以关注我们腾讯云技术社区 - 云加社区官方号及知乎机构号

退出移动版