在沙箱 iframe 中使用 eval()

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

不過,我們瞭解許多程式庫會使用 eval()eval 類似的建構函式 (例如 new Function()),以提升效能並簡化運算式。範本程式庫特別容易出現這種實作方式。雖然部分架構 (例如 Angular.js) 支援 CSP,但許多熱門架構尚未更新至與擴充功能「無 eval」世界相容的機制。因此,移除這項功能的支援對開發人員來說,比預期更麻煩

本文將介紹沙箱機制,說明如何安全地在專案中納入這些程式庫,同時確保安全性。

為什麼要使用沙箱?

在擴充功能中,eval 是危險的,因為它執行的程式碼可以存取擴充功能高權限環境中的所有內容。有許多強大的 chrome.* API 可供使用,但這些 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 時,系統會載入 Handlebars 程式庫,並以 Handlebars 建議的方式建立及編譯內嵌範本:

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 資料執行一些有趣的操作。在本例中,我們只會透過通知回傳,但完全可以在擴充功能的 UI 中安全地使用這個 HTML。透過 innerHTML 插入內容不會造成重大安全風險,因為我們信任在沙箱中算繪的內容。

這個機制可簡化範本製作程序,但當然不限於範本製作。凡是無法在嚴格的內容安全政策下立即運作的程式碼,都可以進行沙箱化;事實上,為了將程式的每個部分限制在執行時所需的最小權限集,通常會對能夠正確執行的擴充功能元件進行沙箱化。Google I/O 2012 的「撰寫安全的網路應用程式和 Chrome 擴充功能」簡報,提供了一些實際運用這些技巧的絕佳範例,值得花 56 分鐘觀看。