在沙盒化 iframe 中使用 eval()

Chrome 的扩展程序系统会强制执行相当严格的默认内容安全政策 (CSP)。 政策限制非常简单:脚本必须移出线,放入单独的 JavaScript 文件中,内嵌事件处理脚本必须转换为使用 addEventListener,并且 eval() 处于停用状态。

不过,我们也了解到,许多库都使用 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 加载到扩展程序页面中。网页的 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 插入它不会构成重大安全风险,因为我们信任在沙盒中呈现的内容。

这种机制可简化模板的使用,但当然不局限于模板。在严格的内容安全政策下,任何无法直接运行的代码都可以放入沙盒中;事实上,将扩展程序中正确运行的组件放入沙盒中通常很有用,这样可以将程序的每个部分限制为仅拥有执行所需的最少特权集。2012 年 Google I/O 大会上的 Writing Secure Web Apps and Chrome Extensions 演示提供了一些很好的示例,展示了这些技术的实际运用,值得您花 56 分钟的时间观看。