跨站脚本 (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 库和小部件的使用。 - 它不基于 URL 允许列表,因此不会受到常见的 CSP 绕过的影响。
- 它阻止不受信任的内联脚本,例如内联事件处理程序或
javascript:
URI。 - 它限制
object-src
以禁用 Flash 等危险插件。 - 它限制
base-uri
以阻止注入<base>
标记。这可以防止攻击者更改从相对 URL 加载的脚本的位置。
采用严格 CSP
要采用严格 CSP,您需要
- 确定您的应用程序应设置基于 nonce 的 CSP 还是基于哈希的 CSP。
- 从严格 CSP 结构部分复制 CSP,并将其设置为整个应用程序的响应标头。
- 重构 HTML 模板和客户端代码,以删除与 CSP 不兼容的模式。
- 部署您的 CSP。
您可以在整个过程中使用 Lighthouse(v7.3.0 及更高版本,带有标志 --preset=experimental
)最佳实践审核,以检查您的网站是否具有 CSP,以及它是否足够严格以有效地抵御 XSS。

第 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 meta 标记不支持报告模式。
在您的应用程序中设置以下 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 的示例
- 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>
元素都必须具有与 CSP 标头中指定的随机 nonce 值匹配的 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 代码实验室中找到这些以及更多此类重构示例
第 4 步(可选):添加回退以支持旧版浏览器
如果您需要支持旧版浏览器
- 使用
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 类型(nonce、哈希、有或没有 '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 中。
开发人员和安全工程师应在代码审查和安全审核期间特别注意此类模式。您可以在内容安全策略:强化和缓解之间成功的混乱中找到有关这些案例的更多详细信息。