在沙盒化 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 安全地用作扩展程序界面的一部分。通过 innerHTML 插入它不会带来严重的安全风险,因为我们信任在沙盒中呈现的内容。

这种机制使模板化变得简单,但它当然不仅限于模板化。任何在严格的内容安全政策下无法开箱即用的代码都可以沙盒化;事实上,对扩展程序的组件进行沙盒化通常很有用,这些组件 可以 正确运行,以便将程序的每个部分限制为正确执行所需的最小权限集。Google I/O 大会 2012 的“编写安全 Web 应用和 Chrome 扩展程序”演示文稿提供了一些很好的实际示例,值得您花 56 分钟的时间观看。