Browser Support
跨站脚本攻击 (XSS) 是一种将恶意脚本注入 Web 应用的能力,十多年来一直是最大的 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';
基于哈希的严格 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,您需要:
- 确定您的应用是否应设置基于 Nonce 或哈希的 CSP。
- 从严格 CSP 结构部分复制 CSP,并将其设置为整个应用的响应标头。
- 重构 HTML 模板和客户端代码,以移除与 CSP 不兼容的模式。
- 部署 CSP。
在此过程中,您可以使用 Lighthouse(v7.3.0 及更高版本,带有标志 --preset=experimental)最佳实践审核来检查您的网站是否具有 CSP,以及该 CSP 是否足够严格,能够有效防范 XSS。
第 1 步:确定您是否需要基于 Nonce 或哈希的 CSP
以下是两种严格 CSP 的工作原理:
基于 Nonce 的 CSP
借助基于 Nonce 的 CSP,您可以在运行时生成一个随机号码,将其包含在 CSP 中,并将其与网页中的每个脚本标记相关联。攻击者无法在您的网页中添加或运行恶意脚本,因为他们需要猜出该脚本的正确随机数。只有在数字不可猜测且在运行时为每个响应新生成时,此方法才有效。
对于在服务器上呈现的 HTML 网页,请使用基于 Nonce 的 CSP。对于这些网页,您可以为每个响应创建一个新的随机号码。
基于哈希的 CSP
对于基于哈希的 CSP,每个内嵌脚本标记的哈希都会添加到 CSP 中。 每个脚本都有不同的哈希。攻击者无法在您的网页中添加或运行恶意脚本,因为该脚本的哈希值必须位于您的 CSP 中才能运行。
对于以静态方式提供的 HTML 网页或需要缓存的网页,请使用基于哈希的 CSP。例如,您可以将基于哈希的 CSP 用于使用 Angular、React 或其他框架构建的单页 Web 应用,这些应用以静态方式提供,无需服务器端渲染。
第 2 步:设置严格的 CSP 并准备脚本
设置 CSP 时,您有以下几种选择:
- 仅报告模式 (
Content-Security-Policy-Report-Only) 或实施模式 (Content-Security-Policy)。在仅报告模式下,CSP 尚不会屏蔽资源,因此您网站上的任何内容都不会损坏,但您可以看到错误并获取有关任何会被屏蔽的内容的报告。在本地设置 CSP 时,这其实并不重要,因为这两种模式都会在浏览器控制台中显示错误。如果说有什么不同的话,那就是实施模式可以帮助您找到草稿 CSP 屏蔽的资源,因为屏蔽资源可能会导致网页看起来损坏。在流程的后期,仅报告模式会变得非常有用(请参阅第 5 步)。 - 标头或 HTML
<meta>标记。对于本地开发,使用<meta>标记可以更方便地调整 CSP 并快速查看其对网站的影响。不过:- 之后,在生产环境中部署 CSP 时,我们建议将其设置为 HTTP 标头。
- 如果您想将 CSP 设置为仅报告模式,则需要将其设置为标头,因为 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 随机数必须:
- 一个加密强度高的随机值(理想情况下长度为 128 位以上)
- 每次回答都会重新生成
- Base64 编码
以下示例展示了如何在服务器端框架中添加 CSP Nonce:
- Django (python)
- Express (JavaScript):
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 允许它们。
在应用中设置以下 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}'。
动态加载源脚本
您可以使用内嵌脚本动态加载第三方脚本。
<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>{HASHED_INLINE_SCRIPT} 占位符。为了减少哈希数量,您可以将所有内嵌脚本合并为一个脚本。如需查看实际效果,请参阅此示例及其代码。
<script src="https://example.org/foo.js"></script> <script src="https://example.org/bar.js"></script>
integrity 属性。
脚本加载注意事项
内嵌脚本示例添加了 s.async = false,以确保 foo 在 bar 之前执行,即使 bar 先加载也是如此。在此代码段中,s.async = false 不会在脚本加载时阻止解析器,因为脚本是动态添加的。解析器仅在脚本执行时停止,就像执行 async 脚本一样。不过,对于此代码段,请注意以下几点:
-
一个或两个脚本可能会在文档下载完成之前执行。如果您希望在脚本执行时文档已准备就绪,请在附加脚本之前等待
DOMContentLoaded事件。如果这导致脚本下载不够早,从而引发性能问题,请在网页中更早的位置使用预加载标记。 -
defer = true不执行任何操作。如果您需要该行为,请在需要时手动运行脚本。
第 3 步:重构 HTML 模板和客户端代码
内嵌事件处理脚本(例如 onclick="…"、onerror="…")和 JavaScript URI (<a href="javascript:…">) 可用于运行脚本。这意味着,发现 XSS 漏洞的攻击者可以注入此类 HTML 并执行恶意 JavaScript。基于 Nonce 或哈希的 CSP 禁止使用此类标记。
如果您的网站使用了上述任何模式,您需要将其重构为更安全的替代方案。
如果您在上一步中启用了 CSP,则每当 CSP 屏蔽不兼容的模式时,您都可以在控制台中看到 CSP 违规行为。
在大多数情况下,修复方法很简单:
重构内嵌事件处理脚本
<span id="things">A thing.</span>
<script nonce="${nonce}">
document.getElementById('things').addEventListener('click', doThings);
</script><span onclick="doThings();">A thing.</span>
重构 javascript: URI
<a id="foo">foo</a>
<script nonce="${nonce}">
document.getElementById('foo').addEventListener('click', linkClicked);
</script><a href="javascript:linkClicked()">foo</a>
从 JavaScript 中移除 eval()
如果您的应用使用 eval() 将 JSON 字符串序列化转换为 JS 对象,则应将此类实例重构为 JSON.parse(),这样也更快。
如果您无法移除 eval() 的所有使用,您仍然可以设置严格的基于 Nonce 的 CSP,但您必须使用 'unsafe-eval' CSP 关键字,这会使您的政策安全性略有降低。
您可以在此严格 CSP Codelab 中找到这些示例以及更多此类重构示例:
第 4 步(可选):添加后备广告素材以支持旧版浏览器
Browser Support
如果您需要支持旧版浏览器,请执行以下操作:
- 使用
strict-dynamic时,需要添加https:作为早期版本 Safari 的后备。执行此操作后:- 所有支持
strict-dynamic的浏览器都会忽略https:后备,因此这不会降低政策的强度。 - 在旧版浏览器中,外部来源的脚本只有在来自 HTTPS 来源时才能加载。这种方式的安全性不如严格的 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 'none';
base-uri 'none';
第 5 步:部署 CSP
确认 CSP 不会阻止本地开发环境中的任何合法脚本后,您可以将 CSP 部署到预演环境,然后再部署到生产环境:
- (可选)使用
Content-Security-Policy-Report-Only标头以仅报告模式部署 CSP。在开始强制执行 CSP 限制之前,仅报告模式非常适合在生产环境中测试可能具有破坏性的更改(例如新的 CSP)。在仅报告模式下,CSP 不会影响应用的行为,但当浏览器遇到与 CSP 不兼容的模式时,仍会生成控制台错误和违规报告,以便您了解最终用户会遇到哪些问题。如需了解详情,请参阅 Reporting API。 - 当您确信 CSP 不会破坏最终用户的网站时,请使用
Content-Security-Policy响应标头部署 CSP。我们建议您在服务器端使用 HTTP 标头设置 CSP,因为这比<meta>标记更安全。完成此步骤后,CSP 就会开始保护您的应用免受 XSS 攻击。
限制
严格的 CSP 通常会提供强大的额外安全层,有助于缓解 XSS。在大多数情况下,CSP 会拒绝 javascript: URI 等危险模式,从而显著减少攻击面。不过,根据您使用的 CSP 类型(nonces、hashes,带或不带 'strict-dynamic'),在某些情况下,CSP 无法很好地保护您的应用:
- 如果您对脚本使用了 Nonce,但直接注入到该
<script>元素的正文或src参数中。 - 如果注入到动态创建的脚本 (
document.createElement('script')) 的位置,包括注入到根据其参数值创建scriptDOM 节点的任何库函数。这包括一些常见的 API,例如 jQuery 的.html(),以及 jQuery < 3.0 中的.get()和.post()。 - 如果旧版 AngularJS 应用中存在模板注入。攻击者如果能够注入 AngularJS 模板,就可以利用该模板执行任意 JavaScript。
- 如果政策包含
'unsafe-eval',则会注入到eval()、setTimeout()和其他一些很少使用的 API 中。
开发者和安全工程师在代码审核和安全审核期间应特别注意此类模式。如需详细了解这些情况,请参阅内容安全政策:在强化和缓解之间成功实现平衡。