内容安全政策

Mike West
Joe Medley
Joe Medley

网站的安全模式源于同源政策。来自 https://mybank.com 的代码应只能访问 https://mybank.com 的数据,而绝不应允许 https://evil.example.com 访问。每个源均与其余网络保持隔离,从而为开发者提供一个可进行构建和操作的安全沙盒。在理论上,这非常棒。在实践中,攻击者已找到聪明的方式来破坏系统。

例如,跨站脚本攻击 (XSS) 攻击会通过诱骗网站随预期内容一起传送恶意代码来绕过同源政策。这是一个非常大的问题,因为浏览器会将网页上显示的所有代码视为该网页安全源的合法组成部分。XSS 备忘单是攻击者可能会用来通过注入恶意代码来违反这种信任的方法的陈旧但具有代表性的跨类方法。如果攻击者成功地注入任意代码,则系统很可能受到了攻击:用户会话数据泄露,应保密的信息被透露给坏人。显然,如果可能的话,我们很愿意阻止这种情况。

本概览重点介绍一个可显著降低现代浏览器中 XSS 攻击的风险和影响的防护功能:内容安全政策 (CSP)。

要点

  • 使用许可名单来告知客户端允许和禁止的内容。
  • 了解可使用哪些指令。
  • 了解这些指令接受哪些关键字。
  • 内嵌代码和 eval() 被视为有害。
  • 向服务器举报政策违规行为,以免执行这些行为。

来源许可名单

浏览器无法区分哪些脚本是应用的一部分,哪些是第三方恶意注入的,因此,XSS 攻击会利用这个漏洞。例如,在本页面来源的上下文中,页面底部的 Google +1 按钮会加载和执行来自 https://apis.google.com/js/plusone.js 的代码。我们信任该代码,但我们不能指望浏览器能够自行判断来自 apis.google.com 的代码很棒,而来自 apis.evil.example.com 的代码可能不太出色。浏览器欣然地下载并执行页面请求的任意代码,而不会考虑其来源。

CSP 不会盲目信任服务器提供的所有内容,而是会定义 Content-Security-Policy HTTP 标头,让您可以创建受信任内容来源的许可名单,并指示浏览器仅执行或呈现来自这些来源的资源。即使攻击者能够找到用于注入脚本的漏洞,由于该脚本与许可名单不匹配,因此也不会执行。

由于我们信任 apis.google.com 传输有效代码,并且我们信任我们自己也能做到,因此,我们可以定义一个政策,该政策仅允许执行来自以下两个来源之一的脚本:

Content-Security-Policy: script-src 'self' https://apis.google.com

很简单,对吧?您可能已猜到,script-src 是一条指令,其用于控制脚本对于某个特定页面所享有的一组权限。我们已指定 'self' 作为一个有效的脚本来源,指定 https://apis.google.com 作为另一个有效的脚本来源。浏览器通过 HTTPS 以及当前页面的来源从 apis.google.com 尽职地下载和执行 JavaScript。

控制台错误:拒绝加载脚本“http://evil.example.com/evil.js”,因为该脚本违反了以下内容安全政策指令:script-src 'self' https://apis.google.com

定义此政策后,浏览器只会引发一个错误,而不会加载来自任何其他来源的脚本。当狡猾的攻击者设法将代码注入您的网站时,他们只会看到一条错误消息,而不是他们期待的成功。

政策适用于各种各样的资源

虽然脚本资源是最明显的安全风险,但 CSP 提供了一组丰富的政策指令,可让您对允许页面加载的资源进行相当精细的控制。您已了解 script-src,因此这个概念应该很清晰了。

我们快速浏览一下其余的资源指令。以下列表显示了指令在第 2 级时的状态。第 3 级规范已发布,但主要浏览器尚未大规模实现

  • base-uri 用于限制可在网页的 <base> 元素中显示的网址。
  • child-src 用于列出适用于工作器和嵌入的帧内容的网址。例如:child-src https://youtube.com 将启用来自 YouTube(而非其他来源)的嵌入视频。
  • connect-src 用于限制可(通过 XHR、WebSockets 和 EventSource)连接的来源。
  • font-src 用于指定可以提供网页字体的来源。Google 的网页字体可通过 font-src https://themes.googleusercontent.com 启用。
  • form-action 用于列出可从 <form> 标记提交的有效端点。
  • frame-ancestors 指定可以嵌入当前页面的来源。此指令适用于 <frame><iframe><embed><applet> 标记。此指令不能在 <meta> 标记中使用,仅适用于非 HTML 资源。
  • frame-src 在级别 2 中已废弃,但在级别 3 中恢复了。如果不存在,则仍会像以前一样回退到 child-src
  • img-src 用于定义可从中加载图像的来源。
  • media-src 用于限制允许传输视频和音频的来源。
  • object-src 可对 Flash 和其他插件进行控制。
  • plugin-types 用于限制页面可以调用的插件种类。
  • report-uri 用于指定在违反内容安全政策时浏览器向其发送报告的网址。此指令不能用于 <meta> 标记。
  • style-srcscript-src 版的样式表。
  • upgrade-insecure-requests 指示 User Agent 将 HTTP 更改为 HTTPS,重写网址架构。此指令适用于具有大量旧网址需要重写的网站。
  • worker-src 是一个 CSP 级别 3 指令,用于限制可作为 worker、Shared Worker 或 Service Worker 加载的网址。自 2017 年 7 月起,此指令的实现方式有限

默认情况下,指令的适用范围很广。如果您没有为指令设置特定的政策(假设为 font-src),则默认情况下,该指令的行为就像您已指定 * 作为有效来源一样(例如,您可以从任何位置加载字体而不受限制)。

您可以通过指定 default-src 指令来替换此默认行为。此指令用于定义您未指定的大多数指令的默认值。一般情况下,这适用于以 -src 结尾的任意指令。如果 default-src 设为 https://example.com,并且您未能指定 font-src 指令,那么,您可以从 https://example.com 加载字体,而不能从任何其他地方加载。在我们前面的示例中,我们仅指定了 script-src,其表示可以从任意来源加载图像、字体等。

以下指令不使用 default-src 作为回退指令。请注意,如果不进行设置,等同于允许运行任何内容。

  • base-uri
  • form-action
  • frame-ancestors
  • plugin-types
  • report-uri
  • sandbox

您可以针对您的特定应用使用任意数量的上述指令,只需在 HTTP 标头中列出每条指令,并使用分号将它们隔开。请确保在一条指令中列出所需的特定类型的全部资源。如果您编写类似 script-src https://host1.com; script-src https://host2.com 的指令,则第二条指令将被忽略。如下指令可正确地将这两个来源指定为有效来源:

script-src https://host1.com https://host2.com

例如,如果您有一个应用从内容分发网络(例如 https://cdn.example.net)加载其所有资源,并且知道您不需要任何框架内容或插件,那么您的政策可能如下所示:

Content-Security-Policy: default-src https://cdn.example.net; child-src 'none'; object-src 'none'

实现细节

您可以在网络上的各种教程中看到 X-WebKit-CSPX-Content-Security-Policy 头文件。今后,您应忽略这些带前缀的标头。现代浏览器(IE 除外)均支持不带前缀的 Content-Security-Policy 标头。您应该使用此类标头。

无论您使用何种标头,都要逐页定义政策:在发送 HTTP 标头的同时您还需要连带发送您希望确保予以保护的每个响应。这样非常灵活,因为您可以根据特定页面的需求调整针对该页面的政策。也许您的网站中的某一组页面具有 +1 按钮,而其他页面则没有:您可以仅在必要时允许加载此按钮代码。

每条指令中的来源列表都是灵活的。您可以按架构 (data:https:) 指定来源,也可按各种具体条件指定来源范围,这些条件从仅限定主机名(example.com,即匹配该主机上的任意来源:任意架构、任意端口)到完全限定 URI(https://example.com:443,仅匹配 HTTPS、仅匹配 example.com 和仅匹配端口 443),不一而足。接受使用通配符,但通配符仅可用作架构、端口,或者仅可位于主机名的最左侧:*://*.example.com:* 将与 example.com 使用任何架构、位于任何端口上的所有子网域(但 example.com 本身除外)匹配。

此来源列表还接受四个关键字:

  • 如您所料,'none' 不执行任何匹配。
  • 'self' 与当前来源(而不是其子网域)匹配。
  • 'unsafe-inline' 允许使用内联 JavaScript 和 CSS。(我们稍后将对此进行详细介绍。)
  • 'unsafe-eval' 允许使用 eval 等文本到 JavaScript 机制。(我们也会介绍这个关键字。)

上述关键字需要使用单引号。例如,script-src 'self'(带引号)可授权执行来自当前主机的 JavaScript;script-src self(无引号)可启用来自名为“self”的服务器(而不是来自当前主机)的 JavaScript,而这可能并非您的本意。

沙盒

还有一条指令值得探讨:sandbox。该指令与我们看到的其他指令有些不同,因为它限制的是页面可进行的操作,而不是页面可加载的资源。如果存在 sandbox 指令,则将此页面视为使用 sandbox 属性在 <iframe> 的内部加载的。这可能会对该页面产生广泛的影响:强制该页面进入一个唯一的来源,同时阻止表单提交等其他操作。上述内容有点超出本文的范畴,但您可以在 HTML5 规范的“沙盒”部分中找到关于有效的沙盒属性的完整详细信息。

元标记

CSP 首选的传输机制是一个 HTTP 标头。不过,在标记中直接设置一个页面政策会非常有用。为此,请使用带有 http-equiv 属性的 <meta> 标记:

<meta
  http-equiv="Content-Security-Policy"
  content="default-src https://cdn.example.net; child-src 'none'; object-src 'none'"
/>

此方法不适用于 frame-ancestorsreport-urisandbox

内联代码被视为是有害的

很明显,CSP 基于许可名单来源,因为这是一种明确的方式,用于指示浏览器将特定资源集视为可接受的资源,并拒绝其余资源。不过,基于来源的许可名单无法解决 XSS 攻击带来的最大威胁:内嵌脚本注入。如果攻击者可以注入直接包含某些恶意载荷 (<script>sendMyDataToEvilDotCom();</script>) 的脚本代码,浏览器将无法通过任何机制将其与合法的内嵌脚本代码区分开来。CSP 可通过完全禁止内联脚本来解决此问题:这是唯一确定有效的方式。

此禁止规则不仅包括在 script 标记中直接嵌入的脚本,也包括内联事件处理程序和 javascript: 网址。您需要将 script 标记的内容移入外部文件,并使用相应的 addEventListener() 调用替换 javascript: 网址和 <a ... onclick="[JAVASCRIPT]">。例如,您可以将以下内容重写为:

<script>
  function doAmazingThings() {
    alert('YOU AM AMAZING!');
  }
</script>
<button onclick="doAmazingThings();">Am I amazing?</button>

重写为下面这样:

<!-- amazing.html -->
<script src="amazing.js"></script>
<button id="amazing">Am I amazing?</button>

<div style="clear:both;"></div>
// amazing.js
function doAmazingThings() {
  alert('YOU AM AMAZING!');
}
document.addEventListener('DOMContentLoaded', function () {
  document.getElementById('amazing').addEventListener('click', doAmazingThings);
});

除了能够更好地配合 CSP 外,重写的代码还具有许多优势;无论您是否使用 CSP,这都是最佳做法。内联 JavaScript 混合结构和行为的方式正是您不应采用的方式。外部资源更易于浏览器缓存,开发者更容易理解,并且有助于编译和缩减大小。如果您将代码移入外部资源,那么您可以编写更好的代码。

以相同方式处理内联样式:style 属性和 style 标记都应合并到外部样式表,以防范可通过 CSS 实现的各种极其狡猾的数据渗漏方法。

如果您必须具有内联脚本和样式,您可以启用它,只需在 script-srcstyle-src 指令中添加 'unsafe-inline' 作为允许的来源。您也可以使用 Nonce 或哈希值(请参阅下文),但您真的不应这样做。 禁止内联脚本是 CSP 提供的最大安全性优势,禁止内联样式同样可以提高应用的安全性。为了确保在将所有代码移出规范之后能够正常运行,需要预先做一些工作,但这是值得进行的一项权衡取舍。

如果您一定要使用它

CSP 级别 2 支持使用加密 Nonce(仅使用一次的数字)或哈希值将特定内嵌脚本添加到许可名单中,从而为内嵌脚本提供向后兼容性。尽管这可能很麻烦,但它在紧急情况下很有用。

如需使用 Nonce,请为您的脚本标记提供 Nonce 属性。该值必须与信任的来源列表中的某个值匹配。例如:

<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">
  // Some inline code I can't remove yet, but need to asap.
</script>

现在,将随机数添加到已追加到 nonce- 关键字的 script-src 指令。

Content-Security-Policy: script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'

请注意,必须为每个网页请求重新生成 Nonce,并且 Nonce 必须是不可猜测的。

哈希值的工作方式与此大致相同。创建脚本自身的 SHA 哈希值并将其添加到 script-src 指令,而不是为 script 标记添加代码。例如,假设您的网页包含以下内容:

<script>
  alert('Hello, world.');
</script>

您的政策将包含以下内容:

Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

这里有几点需要注意。sha*- 前缀指定生成此哈希值的算法。在上面的示例中,使用了 sha256-。CSP 还支持 sha384-sha512-。生成此哈希值时,请勿添加 <script> 标记。大写字母和空格也同样重要,包括前导空格或结尾空格。

使用 Google 搜索如何生成 SHA 哈希,可以找到任何语言的解决方法。使用 Chrome 40 或更高版本,您可以打开 DevTools,然后重新加载您的页面。Console 标签将包含错误消息,提供每个内联脚本的正确的 sha256 哈希值。

Eval 同样有害

即使攻击者无法直接注入脚本,也可能会诱骗您的应用将原本不活跃的文本转换为可执行的 JavaScript,并代表攻击者执行该脚本。eval()、新 Functions()、setTimeout([string], ...)setInterval([string], ...) 都是向量,注入的文本最终可能会通过这些向量执行意外的恶意内容。CSP 对于此风险的默认响应是完全阻止所有这些矢量。

这对您构建应用的方式有不小的影响。

  • 您必须通过内置 JSON.parse 解析 JSON,而不是依靠 eval。原生 JSON 操作在 IE8 及以上版本的每个浏览器中均可用,并且十分安全。
  • 重写您当前使用内联函数(而不是字符串)进行的任何 setTimeoutsetInterval 调用。例如:
setTimeout("document.querySelector('a').style.display = 'none';", 10);

最好写为:

setTimeout(function () {
  document.querySelector('a').style.display = 'none';
}, 10);
  • 在运行时避免使用内联模板:为在运行时加快模板生成的速度,许多模板库大量使用 new Function()。这是一个高效的动态编程应用,但在评估恶意文本时存在风险。一些框架开箱即支持 CSP,在缺少 eval 时回退到强大的解析器。AngularJS 的 ng-csp 指令就是一个很好的例子。

不过,最好选择可提供预编译(例如,Handlebars 就可以)的模板语言。预编译您的模板能够让用户体验到甚至比最快的运行时实现还要快的速度,并且也更加安全。如果 eval 及其 text-to-JavaScript 兄弟指令对您的应用非常重要,您可以通过在 script-src 指令中添加 'unsafe-eval' 作为允许的来源来启用它们,但我们强烈建议您不要这样做。禁止能够执行字符串会让攻击者更加难以在您的网站上执行未经授权的代码。

报告

CSP 能够阻止不受信任的资源客户端,这对于您的用户来说是一个巨大的优势,而若能够向服务器返回某种通知以便您可以在第一时间发现和制止允许恶意注入的错误,更是很有帮助。为此,您可以指示浏览器将 JSON 格式的违规行为报告POST到在 report-uri 指令中指定的位置。

Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

这些报告将类似如下:

{
  "csp-report": {
    "document-uri": "http://example.org/page.html",
    "referrer": "http://evil.example.com/",
    "blocked-uri": "http://evil.example.com/evil.js",
    "violated-directive": "script-src 'self' https://apis.google.com",
    "original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
  }
}

其中包含大量有助于您跟踪违规行为的具体原因,包括发生违规行为的网页 (document-uri)、该网页的引荐来源网址(请注意,与 HTTP 标头字段不同的是,键没有拼写错误)、违反网页政策的资源 (blocked-uri)、违反网页政策的具体指令 (violated-directive) 以及网页的完整政策 (original-policy)。

仅报告

如果您刚刚开始使用 CSP,不妨先评估应用的当前状态,然后再向用户发布严苛的政策。为了逐步实现完整部署,您可以要求浏览器监控政策,报告违规情况,但不强制执行限制。您可以发送 Content-Security-Policy-Report-Only 标头,而不是 Content-Security-Policy 标头。

Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

在仅报告模式中指定的政策不会阻止限制的资源,但会向您指定的位置发送违规行为报告。您甚至可以发送两个标头,在强制执行一项政策的同时监控另一项政策。此方式可有效评估更改应用的 CSP 产生的影响:针对新政策启用报告,监控违规行为报告,并修复出现的所有错误;如果您对其效果满意,可开始强制执行新政策。

实际用例

CSP 1 在 Chrome、Safari 和 Firefox 中非常实用,但在 IE 10 中仅得到非常有限的支持。您可以访问 caniuse.com 查看具体信息。CSP Level 2 在 Chrome 40 及更高版本中可用。Twitter 和 Facebook 等大量网站已部署此标头(Twitter 的案例研究值得一读),并为您开始在自己的网站上进行部署制定了相应标准。

为您的应用制定政策的第一步是评估您实际加载的资源。一旦您了解了应用中各项内容的整合方式,就可以基于这些要求设置政策。我们来看几个常见用例,并确定我们如何在 CSP 的保护范围内为它们提供最好的支持。

用例 1:社交媒体微件

  • Google 的 +1 按钮包含一个来自 https://apis.google.com 的脚本,并嵌入一个来自 https://plusone.google.com<iframe>。为嵌入此按钮,您需要一个同时包含这两个来源的政策。一个最低限度的政策应该是 script-src https://apis.google.com; child-src https://plusone.google.com。您还需要确保将 Google 提供的 JavaScript 代码段提取到外部 JavaScript 文件中。如果您有基于第 1 级的政策,并且使用的是 frame-src,则第 2 级要求您将其更改为 child-src。在 CSP 级别 3 中,不再需要这样做。

  • Facebook 的“赞”按钮具有多种实现选项。我们建议您坚持使用 <iframe> 版本,因为它已与网站的其余部分安全地隔离开来。它需要一个 child-src https://facebook.com 指令才能正常运行。请注意,默认情况下,Facebook 提供的 <iframe> 代码会加载相对网址 //facebook.com。对该网址进行更改以明确指定 HTTPS:https://facebook.com。除非迫不得已,否则没有理由使用 HTTP。

  • Twitter 的推文按钮需要访问一个脚本和一个框架,这两个脚本和框架均托管在 https://platform.twitter.com 上。(Twitter 同样默认提供相对网址;在本地复制/粘贴代码时,请修改代码以指定 HTTPS。) 只要将 Twitter 提供的 JavaScript 代码段移至外部 JavaScript 文件,您就可以使用 script-src https://platform.twitter.com; child-src https://platform.twitter.com 了。

  • 其他平台具有类似的要求,可以通过类似方式解决。我们建议只设置一个值为 'none'default-src,并观察您的控制台以确定您需要哪些资源才能使小部件正常运行。

添加多个 widget 非常简单:只需合并政策指令即可,请记住将同一类型的所有资源合并为一条指令。如果所有三个社交媒体小部件您都需要,则此政策应如下所示:

script-src https://apis.google.com https://platform.twitter.com; child-src https://plusone.google.com https://facebook.com https://platform.twitter.com

用例 2:锁定

下面假设您运行了一个银行网站,并希望确保只能加载您自己编写的这些资源。在这种情况下,请先设置一个会屏蔽所有内容的默认政策 (default-src 'none'),然后在此基础上进一步构建。

假设该银行在 https://cdn.mybank.net 从 CDN 加载所有图片、样式和脚本,并通过 XHR 连接到 https://api.mybank.com/ 以拉取各种数据。可使用帧,但仅用于网站的本地页面(无第三方来源)。网站上没有 Flash,也没有字体和 Extra。我们能够发送的最严格的 CSP 标头为:

Content-Security-Policy: default-src 'none'; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net; img-src https://cdn.mybank.net; connect-src https://api.mybank.com; child-src 'self'

用例 3:仅 SSL

一个婚戒论坛管理员想要确保所有资源仅通过安全渠道加载,但不会真正编写很多代码;他没有能力仅仅依靠内联脚本和样式来重写第三方论坛软件的大量代码。以下政策将非常有效:

Content-Security-Policy: default-src https:; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'

即使在 default-src 中指定了 https:,此脚本和样式指令也不会自动继承该来源。每条指令均会完全覆盖该特定资源类型的默认值。

未来展望

内容安全政策级别 2 是候选建议。W3C 的 Web 应用安全工作组已开始着手此规范的下次更新,内容安全政策级别 3

如果您对这些即将发布的功能介绍感兴趣,请查看 public-webappsec@ 邮件列表存档,或亲自加入探讨。

反馈