内容安全政策

Mike West
Joe Medley
Joe Medley

网络的安全模型以同源政策为基础。https://mybank.com 中的代码应仅有权访问 https://mybank.com 的数据,https://evil.example.com 绝不应有权访问。每个源都与 Web 的其余部分隔离,为开发者提供了一个安全的沙盒,供其构建和玩耍。从理论上讲,这非常棒。在实践中,攻击者已经找到了巧妙的方法来破坏该系统。

例如,跨站脚本攻击 (XSS) 会通过诱骗网站将恶意代码与预期内容一起传送来绕过同源政策。这是一个巨大的问题,因为浏览器会信任网页上显示的所有代码,认为它们是该网页安全来源的合法组成部分。XSS 备忘单是一篇内容已过时但具有代表性的文章,其中列出了攻击者可能会通过注入恶意代码来违反这种信任的方法。如果攻击者成功注入了任何代码,那就意味着游戏结束了:用户会话数据遭到破坏,并且应保密的信息会被泄露给黑客。我们当然希望尽可能避免这种情况。

本文概要介绍了一种防御措施:内容安全政策 (CSP),它可以显著降低在现代浏览器中遭遇 XSS 攻击的风险和影响。

要点

  • 使用许可名单告知客户端允许和禁止的操作。
  • 了解可用的指令。
  • 了解它们接受的关键字。
  • 内嵌代码和 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、WebSocket 和 EventSource)。
  • font-src 用于指定可以提供 Web 字体的来源。您可以通过 font-src https://themes.googleusercontent.com 启用 Google 的 Web 字体。
  • 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 指令,用于限制可作为 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) 的任意精确程度。可以使用通配符,但只能用作 scheme、端口或主机名的最左侧位置:*://*.example.com:* 会匹配 example.com 的所有子网域(但匹配 example.com 本身),无论使用哪种 scheme 和端口。

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

  • 正如您所料,'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 哈希值。

Eval 也是

即使攻击者无法直接注入脚本,也可能会诱骗您的应用将原本不活跃的文本转换为可执行的 JavaScript,并代表攻击者执行该脚本。eval()、new Function()、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 及其文本转换为 JavaScript 的兄弟函数对您的应用至关重要,您可以在 script-src 指令中将 'unsafe-eval' 添加为允许的来源,以启用它们,但我们强烈建议您不要这样做。禁止执行字符串会使攻击者在您的网站上执行未经授权的代码变得更加困难。

报告

CSP 能够在客户端阻止不可信的资源,这对您的用户来说是巨大的胜利,但如果能向服务器发送某种通知,将会非常有帮助,这样您就可以从一开始就识别并修复允许恶意注入的所有 bug。为此,您可以指示浏览器将 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 查看详情。从 Chrome 40 版开始,CSP 级别 2 已在 Chrome 中提供。Twitter 和 Facebook 等大型网站已部署该标头(不妨阅读 Twitter 的案例研究),您也可以开始在自己的网站上部署该标准。

若要为应用制定政策,第一步是评估您实际加载的资源。当您认为自己已经掌握了应用中各项内容的组合方式后,请根据这些要求设置政策。我们来详细介绍几个常见的用例,并确定如何在 CSP 的保护范围内最好地支持这些用例。

用例 1:社交媒体 widget

  • 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 正常运行。

添加多个 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、没有字体、没有额外内容。我们可以发送的限制性最高的 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@ 邮寄列表归档,或自行加入讨论。

反馈