内容安全政策

Mike West
Joe Medley
Joe Medley

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

例如,跨站脚本攻击 (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 列出了 worker 的网址和嵌入的帧内容。例如:child-src https://youtube.com 会允许嵌入来自 YouTube 的视频,但无法嵌入来自其他来源的视频。
  • connect-src 会限制您可以连接(通过 XHR、WebSockets 和 EventSource)的来源。
  • font-src 用于指定可以提供网页字体的来源。您可以通过 font-src https://themes.googleusercontent.com 启用 Google 的网页字体。
  • 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)不等。接受通配符,但只能将其作为架构、端口或位于主机名的最左边位置使用:*://*.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 标记的内容移到外部文件中,并将 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,并且随机数必须是不可猜测的。

哈希的工作原理大致相同。请创建脚本本身的 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()、new 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 能够阻止不受信任的资源客户端,这对于您的用户来说是一项巨大的优势,但如果向服务器发送某种通知,以便从一开始就识别并阻止任何允许恶意注入的 bug,将非常有用。为此,您可以指示浏览器将 JSON 格式的违规报告POSTreport-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 上查看具体信息。自版本 40 起,CSP 级别 2 便已在 Chrome 中推出。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 的 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 非常简单:只需合并政策指令即可,并记得将同一类型的所有资源合并到一条指令中。如果您需要所有三个社交媒体微件,则政策应如下所示:

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@ 邮寄名单归档,或亲自参加讨论。

反馈