本文由雲+社群發表作者: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...
此文已由騰訊雲+社群在各渠道釋出
獲取更多新鮮技術乾貨,可以關注我們騰訊雲技術社群-雲加社群官方號及知乎機構號