淺析JavaScript的沙箱內(nèi)容
市面上現(xiàn)在流行兩種沙箱模式,一種是使用iframe,還有一種是直接在頁面上使用new Function + eval進(jìn)行執(zhí)行. 殊途同歸,主要還是防止一些Hacker們 吃飽了沒事干,收別人錢來 Hack 你的網(wǎng)站. 一般情況, 我們的代碼量有60%業(yè)務(wù)+40%安全. 剩下的就看天意了.
接下來,我們來一步一步分析,如果做到在前端的沙箱.文末 看俺有沒有心情放一個彩蛋吧.
直接嵌套
這種方式說起來并不是什么特別好的點子,因為需要花費(fèi)比較多的精力在安全性上.
eval執(zhí)行
最簡單的方式,就是使用eval進(jìn)行代碼的執(zhí)行
- eval('console.log("a simple script");');
 
但,如果你是直接這么使用的話, congraduations... do die...
因為,eval 的特性是如果當(dāng)前域里面沒有,則會向上遍歷.一直到最頂層的global scope 比如window.以及,他還可以訪問closure內(nèi)的變量.看demo:
- function Auth(username)
 - {
 - var password = "trustno1";
 - this.eval = function(name) { return eval(name) } // 相當(dāng)于直接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來調(diào)用,而且他的性能比eval要好很多. 那有沒有辦法解決global var的辦法呢?
有啊... 只是有點復(fù)雜先用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)
 - }
 
這樣,所有的內(nèi)容多會從sandbox這個str上面獲取,但是找不到的var則又會向上進(jìn)行搜索. 為了解決這個問題,則需要使用: 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)
 - }
 - }
 - // 相當(dāng)于檢查 獲取的變量是否在里面 like: 'in'
 - function has (target, key) {
 - return true
 - }
 - compileCode('log(name)')(console);
 
// 相當(dāng)于檢查 獲取的變量是否在里面 like: 'in'
- Object.keys(Array.prototype[Symbol.unscopables]);
 - // ["copyWithin", "entries", "fill", "find", "findIndex",
 - // "includes", "keys", "values"]
 
這樣的話,就能完美的解決掉 向上查找變量的煩惱了. 另外一些大神,發(fā)現(xiàn)在新的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之類的方法,就可以保證安全... 算了,就當(dāng)我沒說,真的沒啥用...
 - if (key === Symbol.unscopables) return undefined
 - return target[key]
 - }
 
現(xiàn)在,基本上就可以宣告你的代碼是99.999% 的5位安全數(shù).(反正不是100%就行)
設(shè)置緩存
如果上代碼,每次編譯一次code時,都會實例一次Proxy, 這樣做會比較損性能. 所以,我們這里,可以使用closure來進(jìn)行緩存。 上面生成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)
 - }
 - })()
 - }
 
不過上面,這樣的緩存機(jī)制有個弊端,就是不能存儲多個proxy. 不過,你可以使用Array來解決,或者更好的使用Map. 這里,我們兩個都不用,用WeakMap來解決這個problem. WeakMap 主要的問題在于,他可以完美的實現(xiàn),內(nèi)部變量和外部的內(nèi)容的統(tǒng)一. WeakMap最大的特點在于,他存儲的值是不會被垃圾回收機(jī)制關(guān)注的. 說白了, WeakMap引用變量的次數(shù)是不會算在引用垃圾回收機(jī)制里, 而且, 如果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))
 - }
 - }
 
差不多了, 如果不嫌寫的丑,可以直接拿去用.(如果出事,純屬巧合,本人概不負(fù)責(zé)).
接著,我們來看一下,如果使用iframe,來實現(xiàn)代碼的編譯. 這里,Jsfiddle就是使用這種辦法.
iframe 嵌套
最簡單的方式就是,使用sandbox屬性. 該屬性可以說是真正的沙盒... 把sandbox加載iframe里面,那么,你這個iframe基本上就是個標(biāo)簽而已... 而且支持性也挺棒的,比如IE10.
- <iframe sandbox src=”...”></iframe>
 
這樣已添加,那么下面的事,你都不可以做了:
1. script腳本不能執(zhí)行
2. 不能發(fā)送ajax請求
3. 不能使用本地存儲,即localStorage,cookie等
4. 不能創(chuàng)建新的彈窗和window, 比如window.open or target="_blank"
5. 不能發(fā)送表單
6. 不能加載額外插件比如flash等
7. 不能執(zhí)行自動播放的tricky. 比如: autofocused, autoplay
看到這里,我也是醉了。 好好的一個iframe,你這樣是不是有點過分了。 不過,你可以放寬一點權(quán)限。在sandbox里面進(jìn)行一些簡單設(shè)置
- <iframe sandbox=”allow-same-origin” src=”...”></iframe>
 
常用的配置項有:
| 配置 | 效果 | 
| allow-forms | 允許進(jìn)行提交表單 | 
| allow-scripts | 運(yùn)行執(zhí)行腳本 | 
| allow-same-origin | 允許同域請求,比如ajax,storage | 
| allow-top-navigation | 允許iframe能夠主導(dǎo)window.top進(jìn)行頁面跳轉(zhuǎn) | 
| allow-popups | 允許iframe中彈出新窗口,比如,window.open,target="_blank" | 
| allow-pointer-lock | 在iframe中可以鎖定鼠標(biāo),主要和鼠標(biāo)鎖定有關(guān) | 
可以通過在sandbox里,添加允許進(jìn)行的權(quán)限.
- <iframe sandbox=”allow-forms allow-same-origin allow-scripts” src=”...”></iframe>
 
這樣,就可以保證js腳本的執(zhí)行,但是禁止iframe里的javascript執(zhí)行top.location = self.location。 更多詳細(xì)的內(nèi)容,請參考: please call me HR.
接下來,我們來具體講解,如果使用iframe來code evaluation. 里面的原理,還是用到了eval.
iframe 腳本執(zhí)行
上面說到,我們需要使用eval進(jìn)行方法的執(zhí)行,所以,需要在iframe上面添加上, allow-scripts的屬性.(當(dāng)然,你也可以使用new Function, 這個隨你...)
這里的框架是使用postMessage+eval. 一個用來通信,一個用來執(zhí)行.
先看代碼:
- <!-- frame.html -->
 - <!DOCTYPE html>
 - <html>
 - <head>
 - <title>Evalbox's Frame</title>
 - <script>
 - window.addEventListener('message', function (e) {
 - // 相當(dāng)于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>
 
這里順便插播一下關(guān)于postMessage的相關(guān)知識點.
postMessage 講解
postMessage主要做的事情有三個:
1.頁面和其打開的新窗口的數(shù)據(jù)傳遞
2.多窗口之間消息傳遞
3.頁面與嵌套的iframe消息傳遞
具體的格式為
- otherWindow.postMessage(message, targetOrigin, [transfer]);
 
message是傳遞的信息,targetOrigin指定的窗口內(nèi)容,transfer取值為Boolean 表示是否可以用來對obj進(jìn)行序列化,相當(dāng)于JSON.stringify, 不過一般情況下傳obj時,會自己先使用JSON進(jìn)行seq一遍.
具體說一下targetOrigin.
targetOrigin的寫入格式一般為URI,即, protocol+host. 另外,也可以寫為*. 用來表示 傳到任意的標(biāo)簽頁中.
另外,就是接受端的參數(shù).接受傳遞的信息,一般是使用window監(jiān)聽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個參數(shù):
- data: 傳遞過來的數(shù)據(jù). e.data
 - origin: 發(fā)送信息的URL, 比如: https://example.org
 - source: 發(fā)送信息的源頁面的window對象. 我們實際上只能從上面獲取信息.
 
該API常常用在window和iframe的信息交流當(dāng)中.
現(xiàn)在,我們回到上面的內(nèi)容.
- <!-- frame.html -->
 - <!DOCTYPE html>
 - <html>
 - <head>
 - <title>Evalbox's Frame</title>
 - <script>
 - window.addEventListener('message', function (e) {
 - // 相當(dāng)于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里面,已經(jīng)做好文檔的監(jiān)聽,然后,我們現(xiàn)在需要進(jìn)行內(nèi)容的發(fā)送.直接在index.html寫入:
- // html部分
 - <textarea id='code'></textarea>
 - <button id='safe'>eval() in a sandboxed frame.</button>
 - // 設(shè)置基本的安全特性
 - <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, '/'); // 只想同源的標(biāo)簽頁發(fā)送
 - }
 - document.getElementById('safe').addEventListener('click', evaluate);
 - // 同時設(shè)置接受部分
 - window.addEventListener('message',
 - function (e) {
 - var frame = document.getElementById('sandboxed');
 - // 進(jìn)行信息來源的驗證
 - if (e.origin === "null" && e.source === frame.contentWindow)
 - alert('Result: ' + e.data);
 - });
 
實際demo可以參考:H5 ROCK
常用的兩種沙箱模式這里差不多講解完了. 開頭說了文末有個彩蛋,這個彩蛋就是使用nodeJS來做一下沙箱. 比如像 ??途W(wǎng)的代碼驗證,就是放在后端去做代碼的沙箱驗證.
彩蛋--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構(gòu)建出來的sandbox里面,沒有任何可以訪問的全局變量.除了基本的syntax.















 
 
 




 
 
 
 