内容安全策略(CSP)

内容安全策略 (CSP) 是一项有助于预防或最大限度地降低某些类型安全威胁风险的功能。它由网站向浏览器发出的一系列指令组成,这些指令指示浏览器限制构成网站的代码所允许执行的操作。

CSP 的主要用例是控制文档允许加载哪些资源,特别是 JavaScript 资源。这主要用作抵御跨站脚本 (XSS) 攻击的防御措施,攻击者能够将恶意代码注入受害者的网站。

CSP 也可以有其他用途,包括防御点击劫持,并帮助确保网站页面通过 HTTPS 加载。

在本指南中,我们将首先描述 CSP 如何传递给浏览器,以及它在高层面上是什么样子。

然后,我们将描述如何使用它来控制加载的资源以防范 XSS,以及其他用例,例如点击劫持保护升级不安全请求。请注意,不同的用例之间没有依赖关系:如果你想添加点击劫持保护但不想缓解 XSS,你可以只添加该用例的指令。

最后,我们将描述部署 CSP 的策略以及有助于简化此过程的工具。

CSP 概述

CSP 应在 Content-Security-Policy 响应头中传递给浏览器。它应该在所有请求的所有响应上设置,而不仅仅是主文档。

你也可以使用文档 <meta> 元素的 http-equiv 属性来指定它,这对于某些用例来说是一个有用的选项,例如只有静态资源的客户端渲染的单页应用程序,因为这样你就可以避免依赖任何服务器基础设施。但是,此选项不支持所有 CSP 功能。

策略以一系列以分号分隔的指令的形式指定。每个指令控制安全策略的不同方面。每个指令都有一个名称,后跟一个空格,再后跟一个值。不同的指令可以有不同的语法。

例如,考虑以下 CSP

http
Content-Security-Policy: default-src 'self'; img-src 'self' example.com

它设置了两个指令

  • default-src 指令设置为 'self'
  • img-src 指令设置为 'self' example.com

A CSP broken into its directives.

第一个指令 default-src 告诉浏览器只加载与文档同源的资源,除非其他更具体的指令为其他资源类型设置了不同的策略。第二个指令 img-src 告诉浏览器加载与文档同源或从 example.com 提供的图像。

在下一节中,我们将介绍可用于控制资源加载的工具,这是 CSP 的主要功能。

控制资源加载

CSP 可用于控制文档允许加载的资源。这主要用于防范跨站脚本 (XSS) 攻击。

在本节中,我们将首先了解控制资源加载如何帮助防范 XSS,然后了解 CSP 提供的控制加载哪些资源的工具。最后,我们将描述一种特别推荐的策略,称为“严格 CSP”。

XSS 和资源加载

跨站脚本 (XSS) 攻击是指攻击者能够在目标网站的上下文中执行其代码。然后,此代码能够执行网站自身代码可以执行的任何操作,例如

  • 访问或修改网站已加载页面的内容
  • 访问或修改本地存储中的内容
  • 使用用户凭据发出 HTTP 请求,使其能够冒充用户或访问敏感数据

当网站接受可能由攻击者精心制作的某些输入(例如,URL 参数或博客文章上的评论)然后将其包含在页面中而没有净化它时,XSS 攻击是可能的:也就是说,没有确保它不能作为 JavaScript 执行。

网站应通过在将此输入包含在页面中之前对其进行净化来保护自己免受 XSS 的侵害。CSP 提供了补充保护,即使净化失败,它也能保护网站。

如果净化确实失败,注入的恶意代码可以在文档中采取各种形式,包括

  • 链接到恶意源的 <script> 标签

    html
    <script src="https://evil.example.com/hacker.js"></script>
    
  • 包含内联 JavaScript 的 <script> 标签

    html
    <script>
      console.log("You've been hacked!");
    </script>
    
  • 内联事件处理程序

    html
    <img onmouseover="console.log(`You've been hacked!`)" />
    
  • javascript: URL

    html
    <iframe src="javascript:console.log(`You've been hacked!`)"></iframe>
    
  • 不安全 API(如 eval())的字符串参数

    js
    eval("console.log(`You've been hacked!`)");
    

CSP 可以提供针对所有这些的保护。使用 CSP,你可以

  • 定义 JavaScript 文件和其他资源的允许源,有效地阻止从 https://evil.example.com 加载
  • 禁用内联脚本标签
  • 只允许设置了正确 nonce 或 hash 的脚本标签
  • 禁用内联事件处理程序
  • 禁用 javascript: URL
  • 禁用危险 API,如 eval()

在下一节中,我们将介绍 CSP 为执行这些操作而提供的工具。

注意:设置 CSP 不能替代净化输入。网站应该净化输入设置 CSP,从而为 XSS 提供深度防御。

Fetch 指令

Fetch 指令用于指定文档允许加载的特定类别的资源——例如 JavaScript、CSS 样式表、图像、字体等。

不同类型的资源有不同的 fetch 指令。例如

一个特殊的 fetch 指令是 default-src,它为所有未明确列出其指令的资源设置回退策略。

有关完整的 fetch 指令集,请参阅参考文档

每个 fetch 指令都指定为单个关键字 'none' 或一个或多个以空格分隔的源表达式。当列出多个源表达式时:如果任何方法允许该资源,则该资源被允许。

例如,下面的 CSP 设置了两个 fetch 指令

  • default-src 具有单个源表达式 'self'
  • img-src 具有两个源表达式:'self'example.com

CSP diagram showing source expressions

这会产生以下效果

  • 图像必须与文档同源,或者从 example.com 加载
  • 所有其他资源必须与文档同源。

在接下来的几节中,我们将描述一些使用源表达式控制资源加载的方法。请注意,尽管我们分别描述它们,但这些表达式通常可以组合:例如,单个 fetch 指令可能包含 nonce 和主机名。

阻止资源

要完全阻止某种资源类型,请使用 'none' 关键字。例如,以下指令阻止所有 <object><embed> 资源

http
Content-Security-Policy: object-src 'none'

请注意,'none' 不能与特定指令中的任何其他方法结合使用:实际上,如果 alongside 'none' 提供了任何其他源表达式,则它们将被忽略。

随机数

nonce 是限制 <script><style> 资源加载的推荐方法。

使用 nonce,服务器为每个 HTTP 响应生成一个随机值,并将其包含在 script-src 和/或 style-src 指令中

http
Content-Security-Policy:
  script-src 'nonce-416d1177-4d12-4e3b-b7c9-f6c409789fb8'

然后,服务器将此值作为它们打算包含在文档中的所有 <script> 和/或 <style> 标签的 nonce 属性的值。

浏览器比较这两个值,并且仅当它们匹配时才加载资源。其思想是,即使攻击者可以将一些 JavaScript 插入页面,他们也不知道服务器将使用哪个 nonce,因此浏览器将拒绝运行脚本。

要使此方法奏效,攻击者必须无法猜测 nonce。

实际上,这意味着 nonce 必须对每个 HTTP 响应都不同,并且不能可预测。

这反过来意味着服务器不能提供静态 HTML,因为它每次都必须插入新的 nonce。通常,服务器会使用模板引擎来插入 nonce。

这是一个 Express 代码片段,用于演示

js
function content(nonce) {
  return `
    <script nonce="${nonce}" src="/main.js"></script>
    <script nonce="${nonce}">console.log("hello!");</script>
    <h1>Hello world</h1> 
    `;
}

app.get("/", (req, res) => {
  const nonce = crypto.randomUUID();
  res.setHeader("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
  res.send(content(nonce));
});

在每个请求上,服务器生成一个新的 nonce,将其插入 CSP 和返回文档中的 <script> 标签。请注意,服务器

  • 为每个请求生成一个新的 nonce
  • 可以将 nonce 与外部和内联脚本一起使用
  • 对文档中的所有 <script> 标签使用相同的 nonce

重要的是,服务器使用某种模板来插入 nonce,而不是仅仅将它们插入到所有 <script> 标签中:否则,服务器可能会无意中将 nonce 插入到攻击者注入的脚本中。

请注意,nonce 只能用于具有 nonce 属性的元素:即,只有 <script><style> 元素。

哈希值

Fetch 指令还可以使用脚本的哈希值来保证其完整性。使用此方法,服务器

  1. 使用哈希函数(SHA-256、SHA-384 或 SHA-512 之一)计算脚本内容的哈希值
  2. 创建结果的Base64 编码
  3. 附加一个标识所使用的哈希算法的前缀(sha256-sha384-sha512- 之一)。

然后将其结果添加到指令中

http
Content-Security-Policy: script-src 'sha256-cd9827ad...'

当浏览器收到文档时,它会对脚本进行哈希处理,将结果与标题中的值进行比较,并且只有当它们匹配时才加载脚本。

外部脚本还必须包含 integrity 属性才能使此方法生效。

这是一个 Express 代码片段,用于演示

js
const hash1 = "sha256-ex2O7MWOzfczthhKm6azheryNVoERSFrPrdvxRtP8DI=";
const hash2 = "sha256-H/eahVJiG1zBXPQyXX0V6oaxkfiBdmanvfG9eZWSuEc=";

const csp = `script-src '${hash1}' '${hash2}'`;
const content = `
  <script src="./main.js"></script>
  <script>console.log("hello!");</script>
    <h1>Hello world</h1> 
    `;

app.get("/", (req, res) => {
  res.setHeader("Content-Security-Policy", csp);
  res.send(content);
});

请注意:

  • 我们对文档中的每个脚本都有一个单独的哈希值。
  • 对于外部脚本“main.js”,我们还包括 integrity 属性,并赋予它相同的值。
  • 与使用 nonce 的示例不同,CSP 和内容都可以是静态的,因为哈希值保持不变。这使得基于哈希的策略更适合静态页面或依赖客户端渲染的网站。

基于方案的策略

Fetch 指令可以列出一个方案,例如 https:,以允许使用该方案提供的资源。这例如允许策略要求所有资源加载都使用 HTTPS

http
Content-Security-Policy: default-src https:

基于位置的策略

Fetch 指令可以根据资源所在的位置控制资源加载。

关键字 'self' 允许与文档本身同源的资源

http
Content-Security-Policy: img-src 'self'

你还可以指定一个或多个主机名,可能包括通配符,并且只允许从这些主机提供的资源。例如,这可以用于允许从受信任的 CDN 提供内容。

http
Content-Security-Policy: img-src *.example.org

你可以指定多个位置。以下指令只允许与当前文档同源的图像,或从“example.org”的子域提供的图像,或从“example.com”提供的图像

http
Content-Security-Policy: img-src 'self' *.example.org  example.com

内联 JavaScript

如果 CSP 包含 default-srcscript-src 指令,则除非采取额外措施启用,否则不允许执行内联 JavaScript。这包括

  • 页面中 <script> 元素内包含的 JavaScript

    html
    <script>
      console.log("Hello from an inline script");
    </script>
    
  • 内联事件处理程序属性中的 JavaScript

    html
    <img src="x" onerror="console.log('Hello from an inline event handler')" />
    
  • javascript: URL 中的 JavaScript

    html
    <a href="javascript:console.log('Hello from a javascript: URL')"></a>
    

unsafe-inline 关键字可用于覆盖此限制。例如,以下指令要求所有资源同源,但允许内联 JavaScript

http
Content-Security-Policy: default-src 'self' 'unsafe-inline'

警告:开发人员应避免使用 'unsafe-inline',因为它会抵消 CSP 的大部分目的。内联 JavaScript 是最常见的 XSS 向量之一,CSP 的最基本目标之一是防止其不受控制的使用。

如果内联 <script> 元素受 nonce 或哈希保护(如上所述),则允许使用它们。

如果指令包含 nonce 或哈希表达式,则浏览器将忽略 unsafe-inline 关键字。

eval() 和类似 API

与内联 JavaScript 类似,如果 CSP 包含 default-srcscript-src 指令,则不允许执行 eval() 和类似 API。这包括(除其他 API 外)

  • eval() 本身

    js
    eval('console.log("hello from eval()")');
    
  • Function() 构造函数

    js
    const sum = new Function("a", "b", "return a + b");
    
  • setTimeout()setInterval() 的字符串参数

    js
    setTimeout("console.log('hello from setTimeout')", 1);
    

unsafe-eval 关键字可用于覆盖此行为,并且与 unsafe-inline 一样,出于相同的原因:开发人员应避免使用 unsafe-eval。有时很难删除 eval() 的用法:在这种情况下,Trusted Types API 可以通过确保输入符合定义的策略来使其更安全。

unsafe-inline 不同,unsafe-eval 关键字在包含 nonce 或哈希表达式的指令中仍然有效。

严格 CSP

为了控制脚本加载以缓解 XSS,推荐的做法是使用基于nonce哈希的 fetch 指令。这称为严格 CSP。与基于位置的 CSP(通常称为允许列表 CSP)相比,这种类型的 CSP 具有两个主要优势

基于 nonce 的严格 CSP 如下所示

http
Content-Security-Policy:
  script-src 'nonce-{RANDOM}';
  object-src 'none';
  base-uri 'none';

在此 CSP 中,我们

  • 使用 nonce 来控制允许加载哪些 JavaScript 资源
  • 阻止所有对象嵌入
  • 阻止所有使用 <base> 元素设置基本 URI 的情况。

基于哈希的严格 CSP 相同,只是它使用哈希而不是 nonce

http
Content-Security-Policy:
  script-src 'sha256-{HASHED_SCRIPT}';
  object-src 'none';
  base-uri 'none';

如果可以动态生成响应(包括内容本身),则基于 nonce 的指令更容易维护。否则,你需要使用基于哈希的指令。基于哈希的指令的问题在于,如果脚本内容有任何更改,则需要重新计算并重新应用哈希。

strict-dynamic 关键字

如上所述,当你使用不受你控制的脚本时,严格 CSP 很难实现。如果第三方脚本加载任何额外的脚本,或使用任何内联脚本,则这将会失败,因为第三方脚本不会通过 nonce 或哈希。

提供了 strict-dynamic 关键字来帮助解决此问题。它是一个可以包含在 fetch 指令中的关键字,其效果是,如果脚本附加了 nonce 或哈希,则该脚本将允许加载本身没有 nonce 或哈希的其他脚本。也就是说,nonce 或哈希赋予脚本的信任会传递给原始脚本加载的脚本(以及它们加载的脚本等)。

例如,考虑一个如下所示的文档

html
<html lang="en-US">
  <head>
    <script
      src="./main.js"></script>
  </head>
  <body>
    <h1>Example page!</h1>
  </body>
</html>

它包含一个脚本“main.js”,该脚本创建并添加了另一个脚本“main2.js”

js
console.log("hello");

const scriptElement = document.createElement("script");
scriptElement.src = `main2.js`;

document.head.appendChild(scriptElement);

我们使用如下 CSP 提供文档

http
Content-Security-Policy:
  script-src 'sha256-gEh1+8U9S1vkEuQSmmUMTZjyNSu5tIoECP4UXIEjMTk='

“main.js”脚本将允许加载,因为其哈希值与 CSP 中的值匹配。但其加载“main2.js”的尝试将失败。

如果我们将 'strict-dynamic' 添加到 CSP,则“main.js”将允许加载“main2.js”

http
Content-Security-Policy:
  script-src 'sha256-gEh1+8U9S1vkEuQSmmUMTZjyNSu5tIoECP4UXIEjMTk='
  'strict-dynamic'

'strict-dynamic' 关键字使得创建和维护基于 nonce 或哈希的 CSP 变得更加容易,尤其是在网站使用第三方脚本时。但是,它确实会降低 CSP 的安全性,因为如果你包含的脚本根据潜在的 XSS 源创建 <script> 元素,则 CSP 将无法保护它们。

重构内联 JavaScript 和 eval()

我们上面已经看到,CSP 默认不允许内联 JavaScript。使用 nonce 或哈希,开发人员可以使用内联 <script> 标签,但你仍然需要重构代码以删除其他不允许的模式,包括内联事件处理程序、javascript: URL 和 eval() 的用法。例如,内联事件处理程序通常应替换为对 addEventListener() 的调用

html
<p onclick="console.log('Hello from an inline event handler')">click me</p>
html
<!-- served with the following CSP:
 `script-src 'sha256-AjYfua7yQhrSlg807yyeaggxQ7rP9Lu0Odz7MZv8cL0='`
 -->
<p id="hello">click me</p>
<script>
  const hello = document.querySelector("#hello");
  hello.addEventListener("click", () => {
    console.log("Hello from an inline script");
  });
</script>

点击劫持保护

frame-ancestors 指令可用于控制允许哪些文档(如果有)将此文档嵌入到嵌套浏览上下文(例如 <iframe>)中。这是一种有效的点击劫持攻击防御措施,因为这些攻击依赖于将目标网站嵌入到攻击者控制的网站中。

frame-ancestors 的语法是 fetch 指令语法的子集:你可以提供单个关键字值 'none' 或一个或多个源表达式。但是,你唯一可以使用的源表达式是方案、主机名或 'self' 关键字值。

除非你需要网站可嵌入,否则应将 frame-ancestors 设置为 'none'

http
Content-Security-Policy: frame-ancestors 'none'

此指令是 X-Frame-Options 标头更灵活的替代方案。

升级不安全请求

强烈建议网站开发人员通过 HTTPS 提供所有内容。在将网站升级到 HTTPS 的过程中,网站有时会通过 HTTPS 提供主文档,但通过 HTTP 提供其资源,例如,使用如下标记

html
<script src="http://example.org/my-cat.js"></script>

这称为混合内容,不安全资源的存在极大地削弱了 HTTPS 提供的保护。根据浏览器实现的混合内容算法,如果文档通过 HTTPS 提供,则不安全资源分为“可升级内容”和“可阻止内容”。可升级内容升级到 HTTPS,可阻止内容被阻止,可能导致页面中断。

混合内容的最终解决方案是开发人员通过 HTTPS 加载所有资源。然而,即使网站实际上能够通过 HTTPS 提供所有内容,开发人员重写网站用于加载所有资源的 URL 仍然可能非常困难(甚至在涉及存档内容时实际上不可能)。

upgrade-insecure-requests 指令旨在解决此问题。此指令没有任何值:要设置它,只需包含指令名称

http
Content-Security-Policy: upgrade-insecure-requests

如果文档上设置了此指令,则浏览器将在以下情况下自动将任何 HTTP URL 升级到 HTTPS

  • 加载资源的请求(例如图像、脚本或字体)
  • 与文档同源的导航请求(例如链接目标)
  • 嵌套浏览上下文中的导航请求,例如 iframes
  • 表单提交

但是,目标是不同来源的顶级导航请求不会升级。

例如,假设 https://example.org 的文档使用包含 upgrade-insecure-requests 指令的 CSP 提供,并且文档包含如下标记

html
<script src="http://example.org/my-cat.js"></script>
<script src="http://not-example.org/another-cat.js"></script>

浏览器将自动将这两个请求升级到 HTTPS。

假设文档还包含以下内容

html
<a href="http://example.org/more-cats">See some more cats!</a>
<a href="http://not-example.org/even-more-cats">More cats, on another site!</a>

浏览器将第一个链接升级到 HTTPS,但不会升级第二个链接,因为它导航到不同的来源。

此指令不能替代 Strict-Transport-Security 标头(也称为 HSTS),因为它不会将外部链接升级到网站。网站应包含此指令和 Strict-Transport-Security 标头。

测试你的策略

为了简化部署,CSP 可以以仅报告模式部署。策略不强制执行,但任何违规都会发送到策略中指定的报告端点。此外,仅报告标头可用于测试策略的未来修订版,而无需实际部署它。

你可以使用 Content-Security-Policy-Report-Only HTTP 标头来指定你的策略,如下所示

http
Content-Security-Policy-Report-Only: policy

如果同一个响应中同时存在 Content-Security-Policy-Report-Only 标头和 Content-Security-Policy 标头,则两个策略都将生效。Content-Security-Policy 标头中指定的策略将强制执行,而 Content-Security-Policy-Report-Only 策略会生成报告但不强制执行。

请注意,与普通内容安全策略不同,仅报告策略不能在 <meta> 元素中提供。

违规报告

报告 CSP 违规的推荐方法是使用 报告 API,在 Reporting-Endpoints 中声明端点,并使用 Content-Security-Policy 标头的 report-to 指令将其中一个指定为 CSP 报告目标。

警告:你还可以使用 CSP report-uri 指令来指定 CSP 违规报告的目标 URL。这通过 POST 操作发送稍微不同的 JSON 报告格式,其中 Content-Typeapplication/csp-report。此方法已弃用,但在所有浏览器都支持 report-to 之前,你应该同时声明这两个。有关此方法的更多信息,请参阅 report-uri 主题。

服务器可以使用 Reporting-Endpoints HTTP 响应头通知客户端在哪里发送报告。此标头将一个或多个端点 URL 定义为逗号分隔列表。例如,要定义名为 csp-endpoint 的报告端点,该端点在 https://example.com/csp-reports 接受报告,服务器的响应头可能如下所示

http
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"

如果你想拥有处理不同类型报告的多个端点,你可以这样指定它们

http
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports",
                     hpkp-endpoint="https://example.com/hpkp-reports"

然后,你可以使用 Content-Security-Policy 标头的 report-to 指令来指定应使用特定的已定义端点进行报告。例如,要将 CSP 违规报告发送到 https://example.com/csp-reports 用于 default-src,你可能会发送如下所示的响应头

http
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy: default-src 'self'; report-to csp-endpoint

当 CSP 违规发生时,浏览器通过 HTTP POST 操作将报告作为 JSON 对象发送到指定端点,其中 Content-Typeapplication/reports+json。报告是 Report 对象的序列化形式,包含一个 type 属性,其值为 "csp-violation",以及一个作为 CSPViolationReportBody 对象的序列化形式的 body

典型的对象可能如下所示

json
{
  "age": 53531,
  "body": {
    "blockedURL": "inline",
    "columnNumber": 39,
    "disposition": "enforce",
    "documentURL": "https://example.com/csp-report",
    "effectiveDirective": "script-src-elem",
    "lineNumber": 121,
    "originalPolicy": "default-src 'self'; report-to csp-endpoint-name",
    "referrer": "https://www.google.com/",
    "sample": "console.log(\"lo\")",
    "sourceFile": "https://example.com/csp-report",
    "statusCode": 200
  },
  "type": "csp-violation",
  "url": "https://example.com/csp-report",
  "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
}

你需要设置一个服务器来接收具有给定 JSON 格式和内容类型的报告。处理这些请求的服务器然后可以以最适合你需求的方式存储或处理传入的报告。

另见