内容安全政策

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 的指令,用于限制可作为工作器、共享工作器或 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 的 text-to-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' 作为允许的来源。您也可以使用一个随机数或哈希值(见下文),但您真不应这么做。禁止内联脚本是 CSP 提供的最大安全性优势,禁止内联样式同样可以提高应用的安全性。要确保在移除所有不符规范的代码之后能够正常运行,需要预先做一些工作,但这是值得采取的折衷做法。

如果您一定要使用它

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

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

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

现在,将 Nonce 添加到附加到 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()、new Function()、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 更改效果的好方法:为新政策开启报告、监控违规报告并修复出现的所有 bug;如果您对其效果感到满意,即可开始强制执行新政策。

实际用例

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 就大功告成了。

  • 其他平台具有类似的要求,可以通过类似方式解决。我们建议您仅将 default-src 设置为 'none',并观察控制台以确定需要启用哪些资源才能使 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@ 邮件列表存档,或亲自加入探讨。

反馈