假如讓你實現一個線上的 JavaScript 程式碼執行環境,要求使用者程式碼不能對頁面進行修改,以避免潛在的安全問題,你會怎麼做?
使用with
?使用proxy
?OK ,都可以,但是這兩種方法都需要關注很多細節,否則使用者依舊有可乘之機,這樣一來你的實現裡面就會有一個很長長長長長長長的操作黑名單。
除此之外,我們還可以專門部署一個頁面,將程式碼提到服務端渲染成頁面,再透過 iframe 去訪問,如果 iframe 與父頁面之間是跨域的話可以達到很高的安全性——那麼能不能不看後端的臉色,完全使用瀏覽器來實現類似的沙箱呢?
當然可以——
1. iframe
對前端頁面而言,跨域是頁面與頁面之間的鴻溝,但這並不意味著我們必須重新開啟一個頁面來執行新的程式碼,因為我們可以使用 <iframe>
標籤:
<iframe src="www.xxxx.xxx"></iframe>
對於同域的 iframe ,我們可以直接透過 .contentWindow
訪問並操作它的全域性物件,然後直接往裡面執行 JavaScript:
document.querySelector('iframe')
.contentWindow
.eval('alert("hello world!");');
但是同域頁面的子頁面是可以與父頁面進行互操作的,
2. data URL
你可能在一些頁面裡見過小圖片不使用網路連結,而是採用一個 data:image/png;base64 xxxxxxx
風格的 URL ,這種 URL 就是 data URL。
除了 data URL 之外你可能還見過 blob://
開頭的 URL —— Object URL。不過 Object URL 與當前頁面是同域的,而 data URL 與當前頁面是跨域的。所以我們可以在 iframe 使用 data URL 來進行跨域隔離。
3. 將 JavaScript 程式碼變成 data URL
我們可以直接將 JavaScript 片段變成 data:application/javascript,
的 URL ,但是這樣有一個問題: iframe 開啟這樣的 URL 的時候,會顯示程式碼原文而不是執行程式碼,這個行為其實和你直接在瀏覽器位址列輸入 JS 的 URL 是一樣的。
所以我們需要將 JavaScript 程式碼拼接到 html
裡面,再變成 data URL ,然後交給 iframe 去載入:
const javaScriptFragment = `
alert('hello world');
`;
const htmlFragment = `
<!doctype html>
<html>
<head>
<meta chatset="utf-8" />
</head>
<body>
<script>${javaScriptFragment}</script>
</body>
</html>
`;
const dataUrl = `data:text/html,${htmlFragment}`;
// 注意,如果程式碼片段中含有中文的話,需要使用 encodeURIFragment 轉義 htmlFragment
document.querySelector('iframe').src = dataUrl;
4. 如果需要獲取執行結果的話,基於 postMessage 定製通訊機制
如果我們不但要做沙箱隔離,還被要求獲取執行結果的話,則可以做一個通訊機制,讓 iframe 獲取到使用者程式碼執行結果, iframe 與父頁面之間最好的跨域通訊方法莫過於 postMessage
。
除了獲取結果之外,還可以將通訊機制進一步擴充套件成為 RPC ,這樣可以實時修改頁面裡的程式碼來檢視效果,類似於 codepen 。
具體實現與主題不是強相關,這裡就不寫了。