使用严格的内容安全政策 (CSP) 缓解跨站脚本攻击 (XSS)

Lukas Weichselbaum
Lukas Weichselbaum

浏览器支持

  • 52 页
  • 79
  • 52 页
  • 15.4

来源

跨站脚本攻击 (XSS) 可将恶意脚本注入 Web 应用,成为十多年来最大的网络安全漏洞之一。

内容安全政策 (CSP) 是额外的安全层,有助于缓解 XSS 攻击。要配置 CSP,请将 Content-Security-Policy HTTP 标头添加到网页,并设置值来控制用户代理可为该网页加载哪些资源。

本页介绍了如何使用基于 Nonce 或哈希的 CSP 来缓解 XSS,而不是使用基于主机许可名单的常用 CSP,而后者通常会使页面向 XSS 公开,因为在大多数配置中都可以绕过

关键术语:Nonce 是一个仅使用一次的随机数字,可用于将 <script> 标记标记为可信。

关键术语:哈希函数是一个数学函数,用于将输入值转换为称为“哈希”的压缩数值。您可以使用哈希值(例如 SHA-256)将内嵌 <script> 标记标记为可信。

基于 Nonce 或哈希值的内容安全政策通常称为严格 CSP。当应用使用严格的 CSP 时,发现 HTML 注入缺陷的攻击者通常无法使用这些漏洞强制浏览器在存在漏洞的文档中执行恶意脚本。这是因为严格的 CSP 仅允许经过哈希处理的脚本或具有在服务器上生成的正确 Nonce 值的脚本,因此攻击者若不知道给定响应的正确 Nonce,就无法执行脚本。

为什么应使用严格的 CSP?

如果您的网站已有类似 script-src www.googleapis.com 的 CSP,那么它可能对跨网站无效。这种类型的 CSP 称为许可名单 CSP。它们需要进行大量自定义,并且会被攻击者绕过

基于加密 Nonce 或哈希的严格 CSP 可以避免这些陷阱。

严格的 CSP 结构

基本的严格内容安全政策使用以下 HTTP 响应标头之一:

基于 Nonce 的严格 CSP

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
基于 Nonce 的严格 CSP 的工作原理。

基于哈希的严格 CSP

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

以下属性使像这样的 CSP 具有“严格”性质,因此非常安全:

  • 它使用 Nonce 'nonce-{RANDOM}' 或哈希值 'sha256-{HASHED_INLINE_SCRIPT}' 来指示网站开发者信任要在用户的浏览器中执行哪些 <script> 代码。
  • 它会设置 'strict-dynamic',通过自动允许执行可信脚本创建的脚本,来减少部署基于 Nonce 或基于哈希的 CSP 的工作量。这也解锁了大多数第三方 JavaScript 库和 widget 的使用。
  • 它不基于网址许可名单,因此不会遭遇常见的 CSP 绕过
  • 它会阻止不受信任的内嵌脚本,例如内嵌事件处理脚本或 javascript: URI。
  • 它会限制 object-src 停用 Flash 等危险插件。
  • 它会限制 base-uri 以阻止注入 <base> 标记。这样可以防止攻击者更改从相对网址加载的脚本的位置。

采用严格的 CSP

如需采用严格的 CSP,您需要执行以下操作:

  1. 确定您的应用应该设置基于 Nonce 还是基于哈希的 CSP。
  2. 复制严格 CSP 结构部分中的 CSP,并将其设置为应用中的响应标头。
  3. 重构 HTML 模板和客户端代码,以移除与 CSP 不兼容的格式。
  4. 部署您的 CSP。

您可以在整个过程中使用 Lighthouse(v7.3.0 及更高版本,带有 --preset=experimental 标志)最佳实践审核,以检查您的网站是否具有 CSP,以及它是否严格足以应对 XSS。

Lighthouse 报告警告在强制执行模式下找不到 CSP。
如果您的网站没有 CSP,Lighthouse 会显示此警告。

第 1 步:确定您需要基于 Nonce 还是基于哈希的 CSP

这两种类型的严格 CSP 的运作方式如下:

基于 Nonce 的 CSP

对于基于 Nonce 的 CSP,您可以在运行时生成一个随机数,将其添加到您的 CSP 中,然后将其与网页中的每个脚本标记相关联。攻击者无法在您的网页中包含或运行恶意脚本,因为他们需要猜测该脚本的正确随机数。此方法仅在数字无法猜测时才起作用,并且在运行时是为每个响应新生成的。

对在服务器上呈现的 HTML 网页使用基于 Nonce 的 CSP。对于这些页面,您可以为每个响应创建一个新的随机数字。

基于哈希的 CSP

对于基于哈希的 CSP,每个内嵌脚本标记的哈希都会添加到 CSP 中。每个脚本都有不同的哈希值。攻击者无法在您的网页中包含或运行恶意脚本,因为该脚本的哈希值需要位于 CSP 中才能运行。

对于静态提供的 HTML 页面或需要缓存的页面,请使用基于哈希的 CSP。例如,对于使用 Angular、React 等框架构建的单页 Web 应用,您可以使用基于哈希的 CSP,此类应用无需服务器端渲染即可静态提供。

第 2 步:设置严格的 CSP 并准备脚本

在设置 CSP 时,您有以下几种选择:

  • 仅限报告模式 (Content-Security-Policy-Report-Only) 或强制执行模式 (Content-Security-Policy)。在“仅限报告”模式下,CSP 暂时不会阻止资源,因此您网站上的任何内容都不会中断,但您可以查看错误并获取本应被屏蔽的所有内容的报告。当您在本地设置 CSP 时,这并不重要,因为这两种模式都会在浏览器控制台中显示错误。强制执行模式可以帮助您找到草稿 CSP 禁止的资源,因为屏蔽某个资源可能会导致您的网页看起来受损。稍后,“仅报告”模式会变得最实用(请参阅第 5 步)。
  • 标题或 HTML <meta> 标记。对于本地开发,使用 <meta> 标记调整 CSP 并快速查看它对网站的影响会更加方便。但是:
    • 稍后,在生产环境中部署 CSP 时,我们建议将其设置为 HTTP 标头。
    • 如果您想在“仅限报告”模式下设置 CSP,则需要将其设置为标头,因为 CSP 元标记不支持仅限报告模式。

选项 A:基于 Nonce 的 CSP

在您的应用中设置以下 Content-Security-Policy HTTP 响应标头:

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

为 CSP 生成 Nonce

Nonce 是一个随机数字,每次网页加载仅使用一次。只有当攻击者无法猜测出 Nonce 值时,基于 Nonce 的 CSP 才能减少 XSS。CSP nonce 必须符合以下条件:

  • 强加密随机值(长度最好为 128+ 位)
  • 为每个回答新生成
  • Base64 编码

以下示例说明了如何在服务器端框架中添加 CSP Nonce:

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

<script> 元素添加 nonce 属性

对于基于 Nonce 的 CSP,每个 <script> 元素都必须具有一个 nonce 属性,该属性与 CSP 标头中指定的随机 Nonce 值匹配。所有脚本都可以具有相同的 Nonce。第一步是将这些属性添加到所有脚本中,以便 CSP 允许执行。

选项 B:基于哈希的 CSP 响应标头

在您的应用中设置以下 Content-Security-Policy HTTP 响应标头:

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

对于多个内嵌脚本,语法如下所示:'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'

动态加载来源脚本

由于只有内嵌脚本支持各种浏览器中的 CSP 哈希,因此您必须使用内嵌脚本动态加载所有第三方脚本。各浏览器不支持针对来源脚本的哈希值。

有关如何内嵌脚本的示例。
CSP 允许
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
若要允许此脚本运行,您必须计算内嵌脚本的哈希值并将其添加到 CSP 响应标头中,并替换 {HASHED_INLINE_SCRIPT} 占位符。为了减少哈希数量,您可以将所有内嵌脚本合并为一个脚本。如需查看实际操作效果,请参阅此示例及其代码
被 CSP 阻止
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
CSP 会阻止这些脚本,因为只能对内嵌脚本进行哈希处理。

脚本加载注意事项

内嵌脚本示例添加了 s.async = false,以确保 foo 先于 bar 执行,即使 bar 先加载也是如此。在此代码段中,s.async = false 不会在脚本加载时阻止解析器,因为脚本是动态添加的。就像处理 async 脚本一样,解析器只会在脚本执行时停止。不过,对于这个代码段,请注意:

  • 一个或两个脚本可能会在文档下载完成之前执行。如果您希望文档在脚本执行时就已准备就绪,请等待 DOMContentLoaded 事件,然后再附加脚本。如果因为脚本没过早开始下载而导致性能问题,请在网页上尽早使用预加载代码
  • defer = true 不会执行任何操作。如果您需要这样行为,请根据需要手动运行脚本。

第 3 步:重构 HTML 模板和客户端代码

内嵌事件处理脚本(例如 onclick="…"onerror="…")和 JavaScript URI (<a href="javascript:…">) 可用于运行脚本。这意味着,发现 XSS bug 的攻击者可以注入此类 HTML 并执行恶意 JavaScript。基于 Nonce 或哈希的 CSP 禁止使用此类标记。如果您的网站使用了其中的任何格式,您需要将其重构为更安全的替代方案。

如果您在上一步中启用了 CSP,则每次 CSP 阻止不兼容的模式时,都可以在控制台中看到 CSP 违规行为。

Chrome 开发者控制台中的 CSP 违规报告。
与被屏蔽的代码相关的控制台错误。

在大多数情况下,修正方法都很简单:

重构内嵌事件处理脚本

CSP 允许
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
CSP 允许使用使用 JavaScript 注册的事件处理脚本。
被 CSP 阻止
<span onclick="doThings();">A thing.</span>
CSP 会阻止内嵌事件处理脚本。

重构 javascript: URI

CSP 允许
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
CSP 允许使用使用 JavaScript 注册的事件处理脚本。
被 CSP 阻止
<a href="javascript:linkClicked()">foo</a>
CSP 会阻止“javascript: URI”。

从 JavaScript 中移除 eval()

如果您的应用使用 eval() 将 JSON 字符串序列化转换为 JS 对象,则应将此类实例重构为 JSON.parse(),这同样更快

如果您无法移除对 eval() 的所有使用,仍然可以设置基于 Nonce 的严格 CSP,但必须使用 'unsafe-eval' CSP 关键字,这会使政策的安全性略有降低。

您可以在此严格的 CSP Codelab 中找到这些重构以及更多此类重构的示例:

第 4 步(可选):添加回退机制以支持旧版浏览器

浏览器支持

  • 52 页
  • 79
  • 52 页
  • 15.4

来源

如果您需要支持旧版浏览器,请执行以下操作:

  • 如需使用 strict-dynamic,需要添加 https: 作为早期版本的 Safari 的回退机制。执行此操作时:
    • 所有支持 strict-dynamic 的浏览器都会忽略 https: 回退,因此这不会降低政策的强度。
    • 在旧版浏览器中,来自外部的脚本只有在来自 HTTPS 来源时才能加载。这种做法的安全性低于严格的 CSP,但仍能阻止一些常见的 XSS 原因,例如注入 javascript: URI。
  • 为确保与非常旧的浏览器版本(4 年以上)兼容,您可以添加 unsafe-inline 作为后备版本。如果存在 CSP nonce 或 hash,则所有近期使用的浏览器都会忽略 unsafe-inline
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

第 5 步:部署您的 CSP

确认您的 CSP 未阻止本地开发环境中的任何合法脚本后,您可以将 CSP 部署到预演环境,然后再部署到生产环境:

  1. (可选)使用 Content-Security-Policy-Report-Only 标头以“仅限报告”模式部署您的 CSP。在开始强制执行 CSP 限制之前,如果您在生产环境中测试新的 CSP 等潜在破坏性更改,则“仅报告”模式非常方便。在“仅限报告”模式下,您的 CSP 不会影响应用的行为,但当浏览器遇到与 CSP 不兼容的模式时,仍会生成控制台错误和违规报告,以便您查看最终用户的需要。如需了解详情,请参阅 Reporting API
  2. 如果您确信 CSP 不会对最终用户的网站中断,请使用 Content-Security-Policy 响应标头部署 CSP。我们建议您使用 HTTP 标头服务器端设置 CSP,因为它比 <meta> 标记更安全。完成此步骤后,您的 CSP 便会开始保护您的应用免遭 XSS 攻击。

限制

严格的 CSP 通常可提供额外的安全层,有助于减少 XSS。在大多数情况下,CSP 会拒绝 javascript: URI 等危险模式,从而显著减少攻击面。不过,根据您使用的 CSP 类型(随机数、哈希,以及使用或不使用 'strict-dynamic'),在某些情况下,CSP 也无法保护您的应用:

  • 如果您对某个脚本执行了 Nonce 操作,但正文中有直接注入或直接注入了该 <script> 元素的 src 参数,
  • 对动态创建脚本 (document.createElement('script')) 的位置进行注入,包括注入任何根据参数值创建 script DOM 节点的库函数。这包括一些常见的 API,如 jQuery 的 .html(),以及版本低于 3.0 的 jQuery 中的 .get().post()
  • 旧版 AngularJS 应用中是否存在模板注入。可注入到 AngularJS 模板的攻击者可以使用该模板执行任意 JavaScript
  • 如果此政策包含 'unsafe-eval',则会注入 eval()setTimeout() 以及一些其他很少使用的 API。

在代码审核和安全审核期间,开发者和安全工程师应特别关注此类模式。如需详细了解这些情况,请参阅内容安全政策:安全强化和缓解之间取得成功的混乱

深入阅读