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

Lukas Weichselbaum
Lukas Weichselbaum

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

跨站脚本攻击 (XSS) 是一种将恶意脚本注入 Web 应用的能力,十多年来一直是最大的 Web 安全漏洞之一。

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

本页介绍了如何使用基于随机数或哈希的 CSP 来缓解 XSS,而不是使用基于主机许可名单的常用 CSP,因为这些 CSP 经常会使网页暴露于 XSS 风险中,因为它们在大多数配置中都可以被绕过

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

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

基于随机数或哈希的内容安全政策通常称为严格 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&#39;;
  base-uri 'none';
基于随机数的严格 CSP 的运作方式。

基于哈希的严格 CSP

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

以下属性使此类 CSP 成为“严格”的 CSP,从而确保安全性:

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

采用严格的 CSP

如需采用严格的 CSP,您需要:

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

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

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

第 1 步:确定是否需要基于 nonce 或哈希的 CSP

以下是两种严格 CSP 的运作方式:

基于 Nonce 的 CSP

使用基于 Nonce 的 CSP 时,您需要在运行时生成一个随机数,将其包含在 CSP 中,并将其与网页中的每个脚本标记相关联。攻击者无法在您的网页中包含或运行恶意脚本,因为他们需要猜出该脚本的正确随机数。只有当该数字无法猜测,并且在运行时针对每个响应新生成时,此方法才有效。

为在服务器上呈现的 HTML 网页使用基于随机数的 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:基于随机数的 CSP

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

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

为 CSP 生成 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(&>quot;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 值匹配。所有脚本都可以具有相同的随机数。第一步是将这些属性添加到所有脚本中,以便 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 允许
<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.hea<d.appen>dChild(s);
  });
/script
如需运行此脚本,您必须计算内嵌脚本的哈希值,并将其添加到 CSP 响应标头中,替换 {HASHED_INLINE_SCRIPT} 占位符。如需减少哈希数量,您可以将所有内嵌脚本合并为一个脚本。如需查看实际效果,请参阅此示例及其代码
因不符合 CSP 而被屏蔽
<script src="https://example.org/fo><o.js&qu>o<t;/script
script src="https://exam><ple.org>/bar.js"/script
CSP 会屏蔽这些脚本,因为它们不是动态添加的,并且没有与允许的来源匹配的 integrity 属性。

脚本加载注意事项

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

  • 一个或两个脚本可能会在文档下载完成之前执行。如果您希望在脚本执行时文档已准备就绪,请在附加脚本之前等待 DOMContentLoaded 事件。如果这导致了性能问题,因为脚本没有尽早开始下载,请在网页上更早地使用 preload 标记
  • defer = true 不执行任何操作。如果您需要这种行为,请在需要时手动运行脚本。

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

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

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

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

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

重构内嵌事件处理脚本

CSP 允许
<span id="th>ings&quo<t;A t>h<ing./span
script nonce=>"${nonce}"
  document.getElementById('things').addEventL<istener>('click', doThings);
/script
CSP 允许使用 JavaScript 注册的事件处理脚本。
因不符合 CSP 而被屏蔽
<span onclick="doThing>s();&quo<t;A t>hing./span
CSP 会屏蔽内嵌事件处理脚本。

重构 javascript: URI

CSP 允许
<a id=">;fo<o&>q<uot;foo/a
script nonce=>"${nonce}"
  document.getElementById('foo').addEventList<ener(&#>39;click', linkClicked);
/script
CSP 允许使用 JavaScript 注册的事件处理脚本。
因不符合 CSP 而被屏蔽
<a href="javascript:linkClick>ed(<)&>quot;foo/a
CSP 会屏蔽 JavaScript URI。

从 JavaScript 中移除 eval()

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

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

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

第 4 步(可选):添加后备方案以支持旧版浏览器

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

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

  • 使用 strict-dynamic 需要添加 https: 作为旧版 Safari 的后备。执行此操作后:
    • 所有支持 strict-dynamic 的浏览器都会忽略 https: 回退,因此这不会降低政策的强度。
    • 在旧版浏览器中,只有当外部来源的脚本来自 HTTPS 源时,才能加载这些脚本。虽然这种 CSP 的安全性不如严格的 CSP,但仍可防止一些常见的 XSS 原因,例如注入 javascript: URI。
  • 为确保与非常旧的浏览器版本(4 年以上)兼容,您可以添加 unsafe-inline 作为后备。如果存在 CSP nonce 或哈希,所有新版浏览器都会忽略 unsafe-inline
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 9;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 类型(nonce、hash、带或不带 'strict-dynamic'),在某些情况下,CSP 无法很好地保护您的应用:

  • 如果您为脚本添加了 Nonce,但直接向该 <script> 元素的正文或 src 参数注入了内容。
  • 如果存在对动态创建的脚本位置 (document.createElement('script')) 的注入,包括对基于其参数值创建 script DOM 节点的任何库函数的注入。这包括一些常见 API,例如 jQuery 的 .html(),以及 jQuery 3.0 之前的版本中的 .get().post()
  • 如果旧版 AngularJS 应用中存在模板注入。能够注入 AngularJS 模板的攻击者可以使用该模板执行任意 JavaScript
  • 如果政策包含 'unsafe-eval',则会注入到 eval()setTimeout() 和一些其他很少使用的 API 中。

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

深入阅读