内容安全政策

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

Joe Medley
Joe Medley
Mike West

浏览器支持

  • 25
  • 14
  • 23
  • 7

来源

Web 的安全模型基于同源政策。例如,https://mybank.com 中的代码必须只能访问 https://mybank.com 的数据,而绝绝不允许 https://evil.example.com 访问。理论上,每个来源都与 Web 的其余部分隔离开来,从而为开发者提供了一个安全的沙盒进行构建。但实际上,攻击者已经找到了几种破坏系统的方法。

例如,跨站脚本攻击 (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,但不能从其他任何来源下载和执行 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
指定可以提供网页字体的来源。例如,您可以使用 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-src
限制网页可以使用样式表的来源。
upgrade-insecure-requests
指示用户代理通过将 HTTP 更改为 HTTPS 来重写网址 scheme。该指令适用于有大量旧网址需要重写的网站。
worker-src
CSP 级别 3 指令,用于限制可作为 worker、共享 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 按钮,而另一些网页没有 +1 按钮,则您可以允许按钮代码仅在必要时加载。

每个指令的来源列表都是灵活的。您可以按架构(data:https:)或具体程度指定来源,从仅主机名(example.com,该主机上的任何源匹配:任何架构、任何端口)到完全限定的 URI(https://example.com:443,仅匹配 HTTPS、仅匹配 example.com 和仅端口 443)。接受通配符,但只能将其用作 scheme、端口或位于主机名的最左侧位置:*://*.example.com:* 会匹配任何端口上 example.com 的所有子网域(而不是 example.com 本身)。

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

  • 'none' 不匹配项。
  • 'self' 与当前源站匹配,但不匹配其子网域。
  • 'unsafe-inline' 允许内嵌 JavaScript 和 CSS。如需了解详情,请参阅避免使用内嵌代码
  • 'unsafe-eval' 允许使用 eval 等文本到 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 代码的内容移到外部文件中,并将 javascript: 网址和 <a ... onclick="[JAVASCRIPT]"> 替换为适当的 addEventListener() 调用。例如,您可以将以下内容重写为:

<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 级别 2 还允许您使用加密随机数(数字一次使用一次)或哈希将特定的内嵌脚本添加到许可名单中,如下所示。

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

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

将 Nonce 添加到 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 或更高版本,您可以打开开发者工具,然后重新加载页面。“控制台”标签页会显示每个内嵌脚本的错误消息,其中包含正确的 SHA-256 哈希。

避免使用 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' 添加为允许的来源来启用这些函数。我们强烈建议不要这样做,因为它存在代码注入风险。

举报违规行为

如需通知服务器有关可能允许恶意注入的 bug,您可以指示浏览器将 POST JSON 格式的违规行为报告发送到 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)、该网页的 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 所做的更改的好方法:启用新政策的报告、监控违规报告并修复所有 bug,对新政策感到满意后,即可开始强制执行该政策。

实际使用情况

为应用制定政策的第一步是评估应用加载的资源。了解应用的结构后,请根据其要求创建政策。以下部分介绍了一些常见用例以及按照 CSP 准则支持这些用例的决策流程。

社交媒体微件

  • Facebook 的“赞”按钮有多种实现方案。我们建议您使用 <iframe> 版本,以使其与网站的其余部分已沙盒化。它需要一条 child-src https://facebook.com 指令才能正常运行。
  • X 的 Tweet 按钮依赖于对脚本的访问权限。将其提供的脚本移至外部 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,也没有字体和附加功能。它可以发送的限制性最强的 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,该 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@ 邮寄名单归档