在沙箱 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 資料執行有趣的操作。在這種情況下,我們會透過通知回應,但完全可以安全地將這段 HTML 用於擴充功能的 UI。透過 innerHTML 插入內容不會造成重大安全風險,因為我們信任在沙箱中顯示的內容。

這個機制可讓您輕鬆建立範本,但當然不限於範本。不限 如果程式碼無法在嚴格內容安全政策的方塊中執行,就可能會採用沙箱機制。英吋 事實上,對於「必須能正確執行」的擴充功能的沙箱元件來說,這項做法通常非常實用 限制您程式的各個部分僅執行最低限度的權限 以便正確執行2012 年 Google I/O 大會的「Writing Secure Web Apps and Chrome Extensions」簡報提供了這些技術實際應用的實例,值得花 56 分鐘的時間觀看。