内容安全策略(CSP)

为了缓解很大一部分潜在的跨站脚本问题,Chrome 浏览器的扩展程序系统引入了内容安全策略(CSP)的一般概念。这将引入一些相当严格的策略,会使扩展程序在默认情况下更加安全,并使您能够创建并强制应用一些规则,管理您的扩展程序和应用允许加载的内容类型。

大体上,CSP 以白名单/黑名单的机制对您的扩展程序加载或执行的资源起作用。为您的扩展程序定义一项合理的策略使您可以仔细考虑您的扩展程序需要的资源,并且使浏览器确保您的扩展程序只能访问指定的那些资源。这些策略提供了比您的扩展程序请求的主机权限更高的安全性,它们是额外的保护层,而不是替代品。

在网页中,这样的策略通过 HTTP 头信息或者 meta 元素定义。在 Chrome 浏览器的扩展程序系统中,这些都不是合适的方式。扩展程序的策略通过扩展程序的 manifest.json 文件定义,如下所示:

{
  ...,
  "content_security_policy": "[策略字符串写在这里]"
  ...
}

有关 CSP 语法的完整细节,请参见内容安全策略规范(英文),您还可参考 HTML5Rocks 上的内容安全策略简介(英文)这篇文章。

默认策略限制

没有定义 manifest_version(清单文件版本) 的扩展程序包没有默认的内容安全策略,而选择 manifest_version 2 的扩展程序具有如下默认的内容安全策略:

script-src 'self'; object-src 'self'

这一策略通过三种方式限制扩展程序和应用,来增强安全性:

eval 及相关函数已禁用

如下所示的代码不能工作:

alert(eval("foo.bar.baz"));
window.setTimeout("alert('hi')", 10);
window.setInterval("alert('hi')", 10);
new Function("return foo.bar.baz");

像这样对 JavaScript 字符串求值是一种常见的 XSS 攻击载体,而您应该编写如下所示的代码:

alert(foo && foo.bar && foo.bar.baz);
window.setTimeout(function() { alert('hi'); }, 10);
window.setInterval(function() { alert('hi'); }, 10);
function() { return foo && foo.bar && foo.bar.baz };

内嵌 JavaScript 代码将不会执行

内嵌 JavaScript 代码将不会执行。这一限制既禁用了内嵌的 <script>块,同时也包括内嵌的事件处理函数(例如<button onclick="...">)。

第一个限制使您不可能意外地执行任何恶意的第三方提供的脚本,彻底消除了一大部分的跨站脚本攻击。然而,这也确实要求您在编写代码时清晰地将内容与行为分开(这也是您当然应该做的吧?)。举一个例子可能会更加清楚,您可能想要编写一个包含如下内容的 popup.html,作为浏览器按钮的弹出内容

<!doctype html>
<html>
  <head>
    <title>我做的很棒的弹出内容!</title>
    <script>
      function awesome() {
        // 做些很棒的事情!
      }

      function totallyAwesome() {
        // 做些棒极了的事情!
      }

      function clickHandler(element) {
        setTimeout("awesome(); totallyAwesome()", 1000);
      }

      function main() {
        // 在这里进行初始化工作。
      }
    </script>
  </head>
  <body onload="main();">
    <button onclick="clickHandler(this)">
      单击看看会发生什么!
    </button>
  </body>
</html>

有三个地方需要修改,才能使以上代码按照您预期的方式工作:

  • clickHandler 的定义需要移至外部的 JavaScript 文件中(例如 popup.js 就不错)。
  • 内嵌的事件处理器定义必须通过 addEventListener 重写并放在 popup.js 中。

    如果您目前仍然通过类似于 <body onload="main();"> 的代码开始执行您的程序,请考虑通过监听文档的 DOMContentLoaded 事件或 window 的 load 事件来替换它,选择哪一种取决于您的需要。下面我们将使用前一种形式,因为通常它能够更快触发。

  • setTimeout 调用需要重写,避免将字符串 "awesome(); totallyAwesome()" 转换为 JavaScript 来执行。

做出这些更改后代码如下所示:

function awesome() {
  // 做些很棒的事情!
}

function totallyAwesome() {
  // 做些棒极了的事情!
}

function awesomeTask() {
  awesome();
  totallyAwesome();
}

function clickHandler(e) {
  setTimeout(awesomeTask, 1000);
}

function main() {
  // 在这里进行初始化工作。
}

// 通过监听文档的 `DOMContentLoaded` 事件在 DOM 完全加载后添加
// 事件监听器,当事件触发时向指定元素添加您自己的监听器。
document.addEventListener('DOMContentLoaded', function () {
  document.querySelector('button').addEventListener('click', clickHandler);
  main();
});
<!doctype html>
<html>
  <head>
    <title>我做的很棒的弹出内容!</title>
    <script src="popup.js"></script>
  </head>
  <body>
    <button>单击看看会发生什么!</button>
  </body>
</html>

只有本地脚本和对象资源才会加载

脚本与对象资源只能从扩展程序包中加载,而不能从范围更大的网上加载。这样确保您的扩展程序只会执行您确实允许的代码,避免任何主动的网络攻击者,恶意地重定向您对资源的请求。

不要编写依赖于外部 CDN 加载的 jQuery(或其他库)的代码,而应该考虑将特定版本的 jQuery 包含在您的扩展程序包中。即,不要:

<!doctype html>
<html>
  <head>
    <title>我做的很棒的弹出内容!</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
  </head>
  <body>
    <button>单击看看会发生什么!</button>
  </body>
</html>

而应该下载此文件,包含在您的扩展程序包中,并编写如下代码:

<!doctype html>
<html>
  <head>
    <title>我做的很棒的弹出内容!</title>
    <script src="jquery.min.js"></script>
  </head>
  <body>
    <button>单击看看会发生什么!!</button>
  </body>
</html>

放宽默认策略

内嵌脚本

没有办法放宽限制,允许执行内嵌 JavaScript 代码。特别地,设置包含 unsafe-inline 的脚本策略不会生效。

远程脚本

如果您需要某些外部 JavaScript 代码或对象资源,您可以在有限的程度上放宽策略,将安全来源的可接受脚本加入白名单。我们希望确保以扩展程序提升的权限加载的可执行资源一定是您预期的,而没有被主动的网络攻击者替换。由于中间人攻击非常普遍,并且通过 HTTP 无法检测到,这些来源不会被接受。

目前,我们允许将来源为以下协议的资源加入白名单:blobfilesystemhttpschrome-extensionchrome-extension-resourcehttpschrome-extension 协议必须显式指定来源的主机部分,不允许 https:https://*https://*.com 之类的通配符,允许使用类似于 https://*.example.com 的子域名通配符。

为了方便开发,我们也允许将那些通过 HTTP 从本地计算机的服务器上加载的资源列入白名单,您可以将 http://127.0.0.1http://localhost 任意端口上的脚本和对象来源加入白名单。

对于通过 HTTP 加载资源的限制仅仅适用于直接执行的那些资源,您仍然可以,例如,向您希望使用的任何来源发起 XMLHttpRequest 连接,默认策略不会以任何方式限制 connect-src 或者其他任何 CSP 指示符。

允许通过 HTTPS 加载来自 example.com 的脚本资源的放宽策略定义如下所示:

"content_security_policy": "script-src 'self' https://example.com; object-src 'self'"

注意 script-srcobject-src 都由这一策略定义,Chrome 浏览器不会接受不将这些值限制为(至少)'self'的策略。

利用 Google Analytics(分析)是这一种策略定义的典型例子,这样的情况很常见,所以我们在利用 Google Analytics(分析)追踪事件的示例扩展程序中提供了简单例子,并在简明教程中提供了更多详情。

JavaScript 求值

阻止 eval 及类似构造,像 setTimeout(String)setInterval(String) 以及 new Function(String) 的策略也可以通过向您的策略添加 unsafe-eval 来放松:

"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"

然而我们强烈建议您不要这么做。这些函数是臭名昭著的 XSS 攻击载体。

使用更严格的策略

您当然也可以以带来更多不便为代价,使用您的扩展程序允许的更严格的策略,来增强安全性。例如,要指定您的扩展程序只能从自己的包中加载所有类型(如图片等)的资源,可以使用这样的策略:default_src 'self'Mappy 示例扩展程序就是使用的策略比默认设置更加严格的例子。

内容脚本

之前讨论的策略应用于扩展程序的后台网页事件页面,扩展程序的内容脚本中应用这些策略时情况更复杂。

内容脚本通常不受扩展程序 CSP 的限制。内容脚本不是 HTML,这一点的主要影响是它们可以使用 eval,即使扩展程序的 CSP 没有指定 unsafe-eval,尽管并不推荐这么做。此外,网页的 CSP 不适用于内容脚本。更复杂的情况是内容脚本创建并加入所在网页 DOM 中的 <script> 标签,下面我们称之为 DOM 插入脚本。

插入网页后立即执行的 DOM 插入脚本执行的方式与预期一致,例如考虑以下代码所示的内容脚本:

    document.write("<script>alert(1);</script>");
  
这段内容脚本执行 document.write() 时会立即显示 alert,无论网页指定了什么策略它都会执行。

但是在 DOM 插入脚本内部以及对于插入后不会立即执行的脚本来说,情况变得更加复杂。假设我们的扩展程序在某个自己提供了 CSP 的网页上运行,指定了 script-src 'self'。如果内容脚本执行以下代码:

    document.write("<button onclick='alert(1);'>click me</button>'");
  
用户单击该按钮时,onclick 脚本不会运行,因为脚本不是立即执行的,代码要等到单击事件产生时才会解释,并不视为内容脚本的一部分,所以网页的 CSP(而不是扩展程序)限制了它的行为。由于对应的 CSP 并未指定 unsafe-inline,内嵌事件处理程序禁止运行。

在这种情况下,实现期望行为的正确方式是在内容脚本中将函数添加为 onclick 处理程序,如下所示:

    document.write("<button id='mybutton'>click me</button>'");
    var button = document.getElementById('mybutton');
    button.onclick = function() {
      alert(1);
    };
  

如果内容脚本执行以下代码的话就会产生另一种类似的问题:

    var script = document.createElement('script');
    script.innerHTML = 'alert(1);'
    document.getElementById('body').appendChild(script);
  
这种情况下,脚本执行,并弹出对话框。但是,考虑如下情况:
    var script = document.createElement('script');
    script.innerHTML = 'eval("alert(1);")';
    document.getElementById('body').appendChild(script);
  
尽管一开始脚本会运行,eval 的调用则会被阻止,因为尽管一开始的脚本执行是允许的,脚本内的行为受到网页 CSP 的约束。

所以,取决于您在扩展程序中编写 DOM 插入脚本的方式,网页 CSP 的更改可能会影响扩展程序的行为。由于内容脚本不受网页 CSP 影响,您应该尽量将扩展程序的大部分行为放在内容脚本中,而不是 DOM 插入脚本中。