奇技淫巧話前端:使用 iframe 實現與父頁面跨域隔離的 JavaScript 程式碼沙箱

知名噴子發表於2023-05-11
假如讓你實現一個線上的 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
6c750fa806844aeb815b65f984c9ed71.png
除了 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 。
具體實現與主題不是強相關,這裡就不寫了。