内容安全政策

Mike West
Joe Medley
Joe Medley

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

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

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

要点

  • 使用许可名单来告知客户端允许和禁止的内容。
  • 了解可以使用哪些指令。
  • 了解它们使用的关键字。
  • 内嵌代码和 eval() 被视为有害。
  • 先向服务器举报违反政策的行为,然后再强制执行。

来源许可名单

浏览器无法区分属于应用一部分的脚本和被第三方恶意注入的脚本。例如,此网页底部的 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 列出了 worker 和嵌入式框架内容的网址。例如:使用 child-src https://youtube.com 可嵌入来自 YouTube(而非其他来源)的视频。
  • connect-src 用于限制您可以(通过 XHR、WebSocket 和 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 指示用户代理重写网址协议,将 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)。可以使用通配符,但只能以 scheme、端口或主机名的最左边形式使用通配符:*://*.example.com:* 会使用任何 scheme 在任何端口上与 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 标记的内容移至外部文件,并将 javascript: 网址和 <a ... onclick="[JAVASCRIPT]"> 替换为相应的 addEventListener() 调用。例如,您可以将以下内容从以下内容重写:

<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 添加到附加到 nonce- 关键字的 script-src 指令中。

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

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

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

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

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

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

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

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

评估也是

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

这对您构建应用的方式有不少影响:

  • 您必须通过内置的 JSON.parse 解析 JSON,而不是依赖于 eval自 IE8 以来的所有浏览器都支持原生 JSON 操作,因此非常安全。
  • 重写您当前使用内联函数(而不是字符串)进行的任何 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 能够屏蔽不受信任的资源在客户端,这对用户来说是一个巨大的优势,但将某种通知发送回服务器将非常有用,因为这有助于您首先识别并阻止任何允许恶意注入的错误。为此,您可以指示浏览器向 report-uri 指令中指定的位置 POST JSON 格式的违规行为报告。

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

  • Twitter 的 Tweet 按钮依赖于对托管在 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 正常运行。

添加多个 widget 非常简单:只需合并政策指令即可,请记住将同一类型的所有资源合并为一条指令。如果您都需要所有三个社交媒体 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@ 邮寄名单归档,或亲自参与讨论。

反馈