内容安全政策

内容安全政策可显著降低现代浏览器中跨网站脚本攻击的风险和影响。

Joe Medley
Joe Medley
Mike West

浏览器支持

  • Chrome:25.
  • Edge:14。
  • Firefox:23.
  • Safari:7.

来源

网站的安全模式基于同源政策。例如,来自 https://mybank.com 的代码必须只能访问 https://mybank.com 的数据,绝不允许访问 https://evil.example.com。从理论上讲,每个源均与其余网络保持隔离,从而为开发者提供一个可进行构建的安全沙盒。但是,在实践中,攻击者已找到多种破坏系统的方法。

例如,跨站脚本 (XSS) 攻击可通过欺骗网站提供恶意代码和计划好的内容来绕过同源政策。这是一个非常大的问题,因为浏览器会将网页上显示的所有代码视为该网页安全源的合法组成部分。XSS 备忘单是一个旧的但具有代表性的跨会话方法,攻击者可通过该方法注入恶意代码来违背信任。如果攻击者成功地注入任意代码,则表示他们已破坏用户会话并获得了对私密信息的访问权限。

本页简要介绍了内容安全政策 (CSP),它是一种可降低现代浏览器中 XSS 攻击风险和影响的策略。

CSP 的组成部分

如需实现有效的 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,但无法从任何其他来源下载和执行 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
指定可提供网页字体的来源。例如,您可以使用 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-src
限制页面可以使用来自哪些来源的样式表。
upgrade-insecure-requests
指示 User Agent 将 HTTP 更改为 HTTPS,重写网址架构。该指令适用于具有大量旧网址(需要重写)的网站。
worker-src
一种 CSP 级别 3 指令,用于限制可作为 worker、shared worker 或 service worker 加载的网址。自 2017 年 7 月起,此指令的实现方式有限

默认情况下,浏览器会从任何来源加载关联资源,没有任何限制,除非您设置包含特定指令的政策。如需替换默认设置,请指定 default-src 指令。此指令用于定义以 -src 结尾的所有未指定指令的默认值。例如,如果您将 default-src 设置为 https://example.com,并且未指定 font-src 指令,则只能从 https://example.com 加载字体。

以下指令不使用 default-src 作为回退指令。请注意,如果不设置这些属性,则等同于允许加载任何内容:

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

基本 CSP 语法

如需使用 CSP 指令,请在 HTTP 标头中列出这些指令,并以英文冒号分隔。请务必在一条指令中列出所需的特定类型的全部资源,如下所示:

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

以下是多个指令的示例,在本例中,某个 Web 应用从位于 https://cdn.example.net 的内容分发网络加载其所有资源,而不使用框架内容或插件:

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

实现细节

现代浏览器支持不带前缀的 Content-Security-Policy 标头。这是建议使用的标头。您可能会在在线教程中看到 X-WebKit-CSPX-Content-Security-Policy 标头,但它们已被废弃。

CSP 按网页定义。您需要在发送要保护的每个响应时一并发送 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 机制。如需了解详情,请参阅避免 eval()

上述关键字需要使用单引号。例如,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 ARE AMAZING!');
    }
</script>
<button onclick='doAmazingThings();'>Am I amazing?</button>

更改为类似如下的内容:

<!-- amazing.html -->
<script src='amazing.js'></script>
<button id='amazing'>Am I amazing?</button>
// amazing.js
function doAmazingThings() {
    alert('YOU ARE AMAZING!');
}
document.addEventListener('DOMContentLoaded', function () {
    document.getElementById('amazing')
    .addEventListener('click', doAmazingThings);
});

重写的代码不仅与 CSP 兼容,还符合网页设计最佳实践。内联 JavaScript 混合结构和行为的方式会导致代码混乱。缓存和编译也更复杂。将代码移入外部资源可提升网页性能。

我们还强烈建议将内联 style 标记和属性移至外部样式表,以保护您的网站免受基于 CSS 的数据渗漏攻击

如何暂时允许内嵌脚本和样式

您可以在 script-srcstyle-src 指令中将 'unsafe-inline' 添加为允许的来源,以启用内联脚本和样式。CSP Level 2 还允许您使用加密随机数(数字仅使用一次)或哈希值将特定内联脚本添加到许可名单,如下所示。

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

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

将随机数添加到 script-src 指令中的 nonce- 关键字后面:

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> 标记。大写字母和空格也同样重要,包括前导空格和尾随空格。

生成 SHA 哈希值的解决方案支持多种语言。使用 Chrome 40 或更高版本,您可以打开 DevTools,然后重新加载您的页面。“控制台”标签页会显示错误消息,并提供每个内联脚本的正确 SHA-256 哈希值。

避免使用 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() 或其他文本转换为 JavaScript 函数对您的应用至关重要,您可以通过在 script-src 指令中添加 'unsafe-eval' 作为允许的来源来启用它们。我们强烈反对这种做法,因为它会带来代码注入风险。

举报违规情况

如需通知服务器可能允许恶意注入的 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)、相应网页的 referrer、违反该网页政策的资源 (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 指南支持这些情形的决策流程。

社交媒体微件

  • Facebook 的“赞”按钮有多个实现选项。我们建议您使用 <iframe> 版本,以便将其与您网站的其余部分隔离开来。它需要 child-src https://facebook.com 指令才能正常运行。
  • X 的“发推文”按钮需要访问脚本。将其提供的脚本移到外部 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

锁定

对于某些网站,您需要确保只能加载本地资源。以下示例将为银行网站开发 CSP,从阻止所有内容的默认政策 (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'

仅 SSL

以下 CSP 示例适用于希望确保其论坛上的所有资源仅使用安全渠道加载,但编码经验不足且没有资源来重写充满内联脚本和样式的第三方论坛软件的论坛管理员:

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

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

CSP 标准开发

内容安全政策级别 2 是 W3C 推荐的标准。W3C 的 Web 应用安全工作组正在开发此规范的下一次迭代,即内容安全政策级别 3

如需了解这些即将发布的功能的相关讨论,请参阅 public-webappsec@ 邮件列表存档