在沙箱 iframe 中使用 eval()

Chrome 的擴充功能系統強制執行相當嚴格的預設內容安全政策 (CSP)。政策限制很簡單:指令碼必須以內嵌方式移至獨立的 JavaScript 檔案、內嵌事件處理常式需轉換為使用 addEventListener,且 eval() 已停用。

但我們也知道,許多程式庫都會使用 eval()eval 類似的結構 (例如 new Function()) 來最佳化效能並簡化表達效果。範本程式庫特別容易採用這種實作方式。雖然有些 (例如 Angular.js) 本身支援 CSP,但許多熱門架構尚未更新至與擴充功能「無 eval」世界相容的機制。因此,移除對該功能的支援功能已證實開發人員問題超出預期

本文件介紹沙箱機制,做為在專案中納入這些程式庫的安全機制,同時兼顧安全性。

為什麼要使用沙箱?

eval 在擴充功能中執行並不安全,因為執行的程式碼可以存取擴充功能高權限環境中的所有內容。大量功能強大的 chrome.* API 可供使用,可能會對使用者的安全性和隱私權造成重大影響。簡單來說,竊取資料是我們最擔心的。所提供的解決方案是沙箱,eval 可在無法存取擴充功能資料或擴充功能的高價值 API 的情況下執行程式碼。沒有資料,不必使用 API 也不成問題。

具體做法是在擴充功能套件中列出採用沙箱機制的特定 HTML 檔案。每當系統載入沙箱頁面時,都會將網頁移至專屬來源,並拒絕存取 chrome.* API。如果我們透過 iframe 將此沙箱頁面載入擴充功能,我們可以傳遞訊息,並以某種方式處理這些訊息,並等待系統傳回結果。這個簡單的訊息傳遞機制讓我們只需要在擴充功能的工作流程中,安全地加入 eval 驅動的程式碼。

建立及使用沙箱

如要直接探索程式碼,請擷取沙箱範例擴充功能並加以停用。這是一個以 Handlebars 範本建構為基礎的小型訊息 API 實例範例,應該可提供所需的所有資訊。如果你有需要進一步說明 可以參考下面的範例

列出資訊清單中的檔案

每個希望在沙箱中執行的檔案,都必須新增 sandbox 屬性,才能列在擴充功能資訊清單中。這是重要步驟,很容易忘記,因此請仔細檢查您的沙箱檔案是否已列在資訊清單中。在這個範例中,我們為檔案「sandbox.html」採用沙箱機制。資訊清單項目如下所示:

{
  ...,
  "sandbox": {
     "pages": ["sandbox.html"]
  },
  ...
}

載入採用沙箱機制的檔案

為了在沙箱檔案中進行有趣的工作,我們必須在可以使用擴充功能程式碼處理的環境中載入該檔案。這裡,sandbox.html 是透過 iframe 載入擴充功能頁面。該網頁的 javaScript 檔案包含一些程式碼,每當有人點選瀏覽器動作時,該程式碼就會傳送訊息到沙箱中,方法是找出頁面上的 iframe,然後在其 contentWindow 上呼叫 postMessage()。該訊息是包含三種屬性的物件:contexttemplateNamecommand。稍後我們會深入介紹 contextcommand

service-worker.js:

chrome.action.onClicked.addListener(() => {
  chrome.tabs.create({
    url: 'mainpage.html'
  });
  console.log('Opened a tab with a sandboxed page!');
});

extension-page.js:

let counter = 0;
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('reset').addEventListener('click', function () {
    counter = 0;
    document.querySelector('#result').innerHTML = '';
  });

  document.getElementById('sendMessage').addEventListener('click', function () {
    counter++;
    let message = {
      command: 'render',
      templateName: 'sample-template-' + counter,
      context: { counter: counter }
    };
    document.getElementById('theFrame').contentWindow.postMessage(message, '*');
  });

從事危險行為

載入 sandbox.html 時,系統會載入 Handlebar 程式庫,並以 Handlebar 建議的方式建立及編譯內嵌範本:

extension-page.html:

<!DOCTYPE html>
<html>
  <head>
    <script src="mainpage.js"></script>
    <link href="styles/main.css" rel="stylesheet" />
  </head>
  <body>
    <div id="buttons">
      <button id="sendMessage">Click me</button>
      <button id="reset">Reset counter</button>
    </div>

    <div id="result"></div>

    <iframe id="theFrame" src="sandbox.html" style="display: none"></iframe>
  </body>
</html>

sandbox.html:

   <script id="sample-template-1" type="text/x-handlebars-template">
      <div class='entry'>
        <h1>Hello</h1>
        <p>This is a Handlebar template compiled inside a hidden sandboxed
          iframe.</p>
        <p>The counter parameter from postMessage() (outer frame) is:
          </p>
      </div>
    </script>

    <script id="sample-template-2" type="text/x-handlebars-template">
      <div class='entry'>
        <h1>Welcome back</h1>
        <p>This is another Handlebar template compiled inside a hidden sandboxed
          iframe.</p>
        <p>The counter parameter from postMessage() (outer frame) is:
          </p>
      </div>
    </script>

這還不失敗!雖然 Handlebars.compile 最終使用 new Function,但運作方式完全相同,最終在 templates['hello'] 中使用編譯過的範本。

傳回結果

我們可以設定接受擴充功能頁面中指令的訊息事件監聽器,讓這個範本可供使用。我們會使用傳入的 command 來判斷需要完成哪些操作 (您可以想像不是單純轉譯,也許只是建立範本呢?也許是在某種方式管理?),而 context 會直接傳遞至範本進行算繪。轉譯後的 HTML 將會傳回回擴充功能頁面,讓擴充功能日後可以運用其內容:

 <script>
      const templatesElements = document.querySelectorAll(
        "script[type='text/x-handlebars-template']"
      );
      let templates = {},
        source,
        name;

      // precompile all templates in this page
      for (let i = 0; i < templatesElements.length; i++) {
        source = templatesElements[i].innerHTML;
        name = templatesElements[i].id;
        templates[name] = Handlebars.compile(source);
      }

      // Set up message event handler:
      window.addEventListener('message', function (event) {
        const command = event.data.command;
        const template = templates[event.data.templateName];
        let result = 'invalid request';

       // if we don't know the templateName requested, return an error message
        if (template) {
          switch (command) {
            case 'render':
              result = template(event.data.context);
              break;
            // you could even do dynamic compilation, by accepting a command
            // to compile a new template instead of using static ones, for example:
            // case 'new':
            //   template = Handlebars.compile(event.data.templateSource);
            //   result = template(event.data.context);
            //   break;
              }
        } else {
            result = 'Unknown template: ' + event.data.templateName;
        }
        event.source.postMessage({ result: result }, event.origin);
      });
    </script>

返回擴充功能頁面,我們會收到這則訊息,然後利用先前傳遞的 html 資料進行一些有趣的工作。在這種情況下,我們只會透過通知回音,但其實您可以放心使用這個 HTML 當做擴充功能 UI 的一部分。我們信任沙箱中算繪的內容,因此透過 innerHTML 插入並不會造成重大的安全性風險。

這個機制讓建立範本非常簡單,而且不限於範本。凡是不符合嚴格內容安全政策原理規定的程式碼,都能採用沙箱機制;事實上,對於「可以」正確執行的擴充功能元件,沙箱通常也很有用,以便將程式的每個元件限制為正確執行所需的最低權限組合。2012 年 Google I/O 大會的「編寫安全網頁應用程式和 Chrome 擴充功能」簡報提供了這些技巧的實用範例,花 56 分鐘的時間撥出時間。