在 Chrome 扩展程序中使用 eval

Chrome 的扩展程序系统会强制执行相当严格的默认内容安全政策 (CSP)。 政策限制很简单:脚本必须外行移到单独的 JavaScript 文件中,必须转换内嵌事件处理脚本以使用 addEventListenereval() 处于停用状态。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, !</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>

返回 Event 页面后,我们将收到此消息,并会对已传递的 html 数据执行一些有趣的操作。在本例中,我们只需通过桌面通知进行回显,但完全有可能将此 HTML 安全地用作扩展程序界面的一部分。通过 innerHTML 插入此类代码不会造成重大的安全风险,因为即使是通过一些巧妙的攻击来完全入侵沙盒化代码,也无法将危险的脚本或插件内容注入到高权限扩展程序上下文中。

这种机制使模板制作变得简单易行,但当然不限于模板制作。在严格的内容安全政策下,任何无法立即使用的代码都可以被沙盒化;事实上,沙盒化会正常运行的扩展程序组件通常很有用,这样可以将程序的每个部分限制为正确执行所需的最小权限集。2012 年 Google I/O 大会的编写安全的 Web 应用和 Chrome 扩展程序演示文稿为实际运用这些技术提供了一些很好的示例,这场演讲价值 56 分钟。