安全地在 Chrome 扩展程序中使用 eval

Chrome 浏览器的扩展程序系统默认情况下强制实施相当严格的内容安全策略(CSP)。这些策略限制非常直白:内嵌脚本必须移动至单独的 JavaScript 文件,内嵌的事件处理函数必须转换为使用 addEventListener,并且 eval() 已禁用。Chrome 浏览器的应用具有更严格的策略,然而我们对于这些策略提供的安全特性相当满意。

然而我们也承认,出于性能优化以及更简洁的表达,许多库都使用 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 在扩展程序的事件页面eventpage.html)中加载。每当浏览器按钮单击时,eventpage.js 中包含的代码会找到页面中的 iframe,并在它的 contentWindow 上执行 postMessage 方法,向沙箱发送消息。该消息为包含两个属性的对象:contextcommand,我们稍后会详细解释它们。

chrome.browserAction.onClicked.addListener(function() {
 var iframe = document.getElementById('theFrame');
 var message = {
   command: 'render',
   context: {thing: 'world'}
 };
 iframe.contentWindow.postMessage(message, '*');
});

有关 postMessage API 的一般信息,请参考 MDN 上的 postMessage 文档(英文),它非常完整,值得一读。特别注意只有可以序列化的数据才能来回传递,例如函数就不行。

做一些有潜在危险的事

sandbox.html 加载时,它会加载 Handlebars 库,并以 Handlebars 提供的方式创建并编译内嵌模板:

<script src="handlebars-1.0.0.beta.6.js"></script>
<script id="hello-world-template" type="text/x-handlebars-template">
  <div class="entry">
    <h1>Hello, {{thing}}!</h1>
  </div>
</script>
<script>
  var templates = [];
  var source = document.getElementById('hello-world-template').innerHTML;
  templates['hello'] = Handlebars.compile(source);
</script>

以上代码不会失败!尽管 Handlebars.compile 最终会使用 new Function,一切都完全与预期中一样,最终 templates['hello'] 中包含了已编译的模板。

将结果传回

我们将设置消息监听器,从事件页面接收命令,使得该模板可以使用。我们将使用传入的 command 来确定做什么(您可以想象,能做的不仅仅是简单的渲染,也许创建模板?也许用某种方式管理它们?),而 context 将直接传入模板,用于渲染。渲染得到的 HTML 将传回事件页面,以便扩展程序利用它接着做一些有用的事:

<script>
  window.addEventListener('message', function(event) {
    var command = event.data.command;
    var name = event.data.name || 'hello';
    switch(command) {
      case 'render':
        event.source.postMessage({
          name: name,
          html: templates[name](event.data.context)
        }, event.origin);
        break;
  
      // case 'somethingElse':
      //   ...
    }
  });
</script>

回到事件页面,我们会接收到该消息,并利用传递给我们的 html 数据做一些有意义的事。在这一例子中,我们只是通过桌面通知显示它,但是完全可能安全地使用这一 HTML 作为扩展程序用户界面的一部分。通过 innerHTML 将其插入并没有严重的安全风险,即使是通过某些巧妙的攻击完全利用沙箱中的代码,也不可能向高权限的扩展程序环境插入危险的脚本或插件内容。

这一机制很直白地描述了模板的使用,然而它当然不仅仅限于使用模板,所有不能在严格的内容安全策略下直接运行的代码都可以在沙箱中运行。事实上,将您的扩展程序中能够正确运行的各组件限制在沙箱中,可以使您的程序的每一部分都限制在正常执行所需的最小权限下。来自 Google I/O 2012 的演示文稿编写安全的网络应用和 Chrome 扩展程序(英文)提供了利用这些技术的例子,值得花费您 56 分钟的时间。