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

Lukas Weichselbaum
Lukas Weichselbaum

浏览器支持

  • Chrome:52。 <ph type="x-smartling-placeholder">
  • Edge:79。 <ph type="x-smartling-placeholder">
  • Firefox:52。 <ph type="x-smartling-placeholder">
  • Safari:15.4. <ph type="x-smartling-placeholder">

来源

跨站脚本攻击 (XSS) 向 Web 应用注入恶意脚本的功能已经成为 最大的网络安全漏洞

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

本页介绍了如何使用基于 Nonce 或哈希值的 CSP 来减少 XSS。 而不使用经常离开网页的常用基于主机许可名单的 CSP 因为它们在大多数配置中可以被绕过

关键术语:Nonce 是一个随机数,只能使用一次,可用于标记 已将 <script> 个代码标记为受信任。

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

基于 Nonce 或哈希值的内容安全政策通常称为 严格的 CSP。当应用使用严格的 CSP 时,发现 HTML 的攻击者 注入缺陷通常不能利用它们来强制浏览器执行 添加恶意脚本这是因为,只有严格的 CSP 允许经过哈希处理的脚本或在 这样一来,攻击者在不知道正确的 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';
<ph type="x-smartling-placeholder">
</ph>
基于 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 库和微件。
  • 它并非基于网址许可名单,因此不会受到 常见 CSP 绕过方式
  • 它会屏蔽不受信任的内嵌脚本,例如内嵌事件处理脚本或 javascript: URI。
  • 它会限制 object-src 以停用 Flash 等危险插件。
  • 它会限制 base-uri,以阻止 <base> 标记的注入。这可防止 更改从相对网址加载的脚本的位置。

采用严格的 CSP

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

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

您可以使用 Lighthouse (带 --preset=experimental 标志的 v7.3.0 及更高版本)最佳实践审核 检查您的网站是否实施了内容安全政策 能够有效抵御 XSS。

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

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

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

基于 Nonce 的 CSP

使用基于 Nonce 的 CSP 时,您可以在运行时生成一个随机数,并将其包含在 内容安全政策,并将其与网页中的每个脚本标记相关联。攻击者 不能在网页中包含或运行恶意脚本,因为它们需要 猜出该脚本的正确随机数。这只有在 不可猜测,并且是在运行时为每个响应新生成的。

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

基于哈希的 CSP

对于基于哈希的 CSP,每个内嵌脚本标记的哈希都会添加到 CSP。 每个脚本都有不同的哈希。攻击者无法添加或运行 脚本,因为该脚本的哈希值需要包含在 供其运行的 CSP。

对于以静态方式提供的 HTML 页面或需要 缓存内容。例如,您可以将基于哈希的 CSP 用于单页网站 使用 Angular、React 等框架构建的应用, 静态传送,无需服务器端呈现。

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

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

  • “仅报告”模式 (Content-Security-Policy-Report-Only) 或强制执行模式 (Content-Security-Policy).在“仅报告”模式下,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';
<ph type="x-smartling-placeholder">

为 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 匹配的 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 哈希, 您必须使用内嵌脚本动态加载所有第三方脚本。 来源脚本的哈希无法在浏览器之间得到很好的支持

有关如何内嵌脚本的示例。
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>
<ph type="x-smartling-placeholder"></ph> 若要让此脚本运行,您必须计算内嵌脚本的哈希值 并将其添加到 CSP 响应标头中,将 {HASHED_INLINE_SCRIPT} 占位符。如需减少哈希数量,您可以合并所有内嵌内容 转换为单个脚本如需了解实际操作,请参阅此 示例 及其代码
已被 CSP 阻止
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
<ph type="x-smartling-placeholder"></ph> CSP 会屏蔽这些脚本,因为只能对内嵌脚本进行哈希处理。

脚本加载注意事项

内嵌脚本示例添加了 s.async = false,以确保 foobar 之前执行,即使 首先加载 bar。在此代码段中,s.async = false 在脚本加载时不会阻止解析器,因为脚本 动态添加。只有在脚本执行时,解析器才会停止,因为 对 async 脚本会是如此。不过,有了这个代码段 注意:

  • 一个或两个脚本可能会在文档完成之前执行 下载。如果您希望在 脚本执行完毕后,请等待 DOMContentLoaded 事件,然后再 附加脚本。如果这导致性能问题 脚本还未尽早开始下载,请在网页上较早地预加载代码
  • defer = true 不会执行任何操作。如果您需要 请根据需要手动运行脚本。

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

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

如果您在上一步中启用了 CSP,将能够在 控制台。

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

在大多数情况下,解决方法很简单:

重构内嵌事件处理脚本

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

重构 javascript: URI

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

从 JavaScript 中移除 eval()

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

即使无法移除所有使用 eval() 的情况,您仍可设置基于 Nonce 的严格限制 但必须使用 'unsafe-eval' CSP 关键字,这样会使您的 政策安全性略低。

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

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

浏览器支持

  • Chrome:52。 <ph type="x-smartling-placeholder">
  • Edge:79。 <ph type="x-smartling-placeholder">
  • Firefox:52。 <ph type="x-smartling-placeholder">
  • Safari:15.4. <ph type="x-smartling-placeholder">

来源

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

  • 使用 strict-dynamic 需要添加 https: 作为早期版本的后备 Safari 版本。执行此操作后:
    • 所有支持 strict-dynamic 的浏览器都会忽略 https: 回退, 因此不会削弱政策的效力
    • 在旧版浏览器中,外部来源的脚本只有在来自 HTTPS 源。这种方式的安全性不及严格的 CSP, 可防止一些常见的 XSS 原因,例如 javascript: URI 注入。
  • 为了确保与非常旧的浏览器版本(4 年以上)兼容,您可以在添加 unsafe-inline 作为后备选项。最近使用过的所有浏览器都会忽略 unsafe-inline 如果存在 CSP nonce 或 hash。
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 不兼容的模式时生成的违规报告。 以便您了解最终用户在哪些方面会遇到问题。有关 信息,请参阅 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这个 包含一些常见 API,例如 jQuery 的 .html(),以及 .get() 和 jQuery 中的 .post() <3.0。
  • 旧版 AngularJS 应用中是否存在模板注入。攻击者 可以注入 AngularJS 模板 执行任意 JavaScript
  • 如果政策包含 'unsafe-eval',则向 eval() 注入数据, setTimeout() 以及其他一些很少使用的 API。

开发者和安全工程师应特别注意 安全审核期间的监控模式如需了解更多详情,请参阅 内容安全政策:加固和缓解之间的成功中涉及的案例。

深入阅读