严格的内容安全策略 (CSP)
跨站脚本 (XSS) ——将恶意脚本注入 Web 应用程序的能力——十多年来一直是最大的 Web 安全漏洞之一。
内容安全策略 (CSP)是一个附加的安全层,有助于缓解 XSS。配置 CSP 涉及将 Content-Security-Policy HTTP 标头添加到网页并设置值以控制允许用户代理为该页面加载哪些资源。本文解释了如何使用基于 nonce 或哈希的 CSP 来缓解 XSS,而不是常用的基于主机允许列表的 CSP,后者通常会使页面暴露于 XSS,因为它们可以在大多数配置中被绕过。
关键术语
随机数是仅使用一次的随机数,可用于将<script>
标签标记为可信。
散列函数是一种数学函数,可将输入值转换为压缩数值——散列。哈希(例如SHA-256)可用于将内联标记<script>
标记为可信。
严格 CSP
基于随机数或散列的内容安全策略通常称为严格 CSP。当应用程序使用严格的 CSP 时,发现 HTML 注入漏洞的攻击者通常无法使用它们来强制浏览器在易受攻击的文档的上下文中执行恶意脚本。这是因为严格的 CSP 只允许在服务器上生成散列脚本或具有正确 nonce 值的脚本,因此攻击者无法在不知道给定响应的正确 nonce 的情况下执行脚本。
为了保护您的站点免受 XSS 攻击,请确保清理用户输入并将CSP 用作额外的安全层。CSP 是一种深度防御技术,可以防止恶意脚本的执行,但它不能替代避免(并及时修复)XSS 错误。
为什么建议使用严格的 CSP 而不是白名单 CSP
如果您的站点已经有一个如下所示的 CSP:script-src www.googleapis.com,它可能对跨站点脚本无效!这种类型的 CSP 称为允许列表 CSP,它有几个缺点:
- 它需要大量的定制。
- 在大多数配置中可以绕过它。
这使得白名单 CSP 在阻止攻击者利用 XSS 方面通常无效。这就是为什么建议使用基于加密随机数或散列的严格 CSP 的原因,这样可以避免上述陷阱。
- 内容安全策略是一种 Web 平台机制,旨在缓解跨站点脚本 (XSS),这是现代 Web 应用程序中的顶级安全漏洞。在本文中,我们仔细研究了采用 CSP 的实际好处,并确定了实际部署中的重大缺陷,这些缺陷导致 94.72% 的不同策略被绕过。我们基于来自超过 10 亿个主机名的大约 1000 亿个页面的搜索引擎语料库进行全互联网分析;结果涵盖了 1,680,867 台主机上的 CSP 部署以及 26,011 个独特的 CSP 策略——这是迄今为止最全面的研究。我们介绍了 CSP 规范的安全相关方面,并对其威胁模型进行了深入分析,重点是 XSS 保护。
- 然后,我们转向对部署在 Internet 上的策略进行定量分析,以了解它们的安全优势。我们观察到,在加载脚本最常被列入白名单的 15 个域中,有 14 个域包含不安全的端点;因此,75.81% 的不同策略使用允许攻击者绕过 CSP 的脚本白名单。总的来说,我们发现 94.68% 的尝试限制脚本执行的策略是无效的,并且 99.34% 的具有 CSP 的主机使用的策略对 XSS 没有好处。
- 最后,我们提出了“严格动态”关键字,这是对规范的补充,它有助于创建基于密码随机数的策略,而不依赖于域白名单。我们讨论了在复杂应用程序中部署这种基于 nonce 的策略的经验,并为网络作者提供指导以改进他们的策略。
白名单 CSP | 严格的 CSP |
---|---|
不能有效地保护您的网站。❌ | 有效保护您的网站。✅ |
必须高度定制。😓 | 始终具有相同的结构。😌 |
什么是严格的内容安全政策?
严格的内容安全策略具有以下结构,并通过设置以下 HTTP 响应标头之一启用:
基于 Nonce 的严格 CSP
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
基于哈希的严格 CSP
这是严格 CSP 的最精简版本。您需要对其进行调整以使其在浏览器中有效。有关详细信息,请参阅添加回退以支持 Safari 和旧版浏览器。
以下属性使 CSP 与上述“严格”类似,因此是安全的:
- 使用随机数
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 始终具有相同的结构,并且不需要为您的应用程序定制。
采用严格的 CSP
要采用严格的 CSP,您需要:
- 决定您的应用程序是否应该设置基于随机数或散列的 CSP。
- 从What is a strict Content Security Policy部分复制 CSP,并将其设置为整个应用程序的响应标头。
- 重构 HTML 模板和客户端代码以删除与 CSP 不兼容的模式。
- 添加回退以支持 Safari 和旧版浏览器。
- 部署您的 CSP。
您可以在整个过程中使用Lighthouse (v7.3.0 及更高版本--preset=experimental)最佳实践审核来检查您的站点是否具有 CSP,以及它是否足够严格以有效对抗 XSS。
第 1 步:确定是否需要基于随机数或散列的 CSP
有两种类型的严格 CSP,基于随机数和基于哈希。以下是它们的工作方式:
- 基于 Nonce 的 CSP :您在运行时生成一个随机数,将其包含在您的 CSP 中,并将其与页面中的每个脚本标签相关联。攻击者无法在您的页面中包含和运行恶意脚本,因为他们需要猜测该脚本的正确随机数。这仅在数字不可猜测并且在运行时为每个响应新生成的情况下才有效。
- 基于散列的 CSP:每个内联脚本标签的散列被添加到 CSP。请注意,每个脚本都有不同的哈希值。攻击者无法在您的页面中包含和运行恶意脚本,因为该脚本的哈希值需要存在于您的 CSP 中。
选择严格 CSP 方法的标准:
基于 Nonce 的 CSP | 对于在服务器上呈现的 HTML 页面,您可以为每个响应创建一个新的随机令牌 (nonce)。 |
---|---|
基于哈希的 CSP | 对于静态提供的 HTML 页面或需要缓存的页面。例如,使用 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 元标记不支持仅报告模式。
选项 A:基于 Nonce 的 CSP
在您的应用程序中设置以下Content-Security-PolicyHTTP 响应标头:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
警告
将{RANDOM}占位符替换为在每个服务器响应上重新生成的随机nonce 。
为 CSP 生成一个 nonce
随机数是每次页面加载仅使用一次的随机数。仅当攻击者无法猜到nonce 值时,基于 nonce 的 CSP 才能缓解 XSS 。CSP 的随机数必须是:
- 一个加密的强随机值(理想情况下长度为 128+ 位)
- 为每个响应新生成
- Base64 编码
以下是一些关于如何在服务器端框架中添加 CSP 随机数的示例:
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 });
});
}
为元素添加nonce属性<script>
对于基于 nonce 的 CSP,每个<script>
元素都必须具有nonce与 CSP 标头中指定的随机 nonce 值匹配的属性(所有脚本都可以具有相同的 nonce)。第一步是将这些属性添加到所有脚本中:
被 CSP 阻止
<script src="/path/to/script.js"></script>
<script>foo()</script>
CSP 将阻止这些脚本,因为它们没有nonce属性。
CSP 允许
<script nonce="${NONCE}" src="/path/to/script.js"></script>
<script nonce="${NONCE}">foo()</script>
${NONCE}如果替换为与 CSP 响应标头中的 nonce 匹配的值,CSP 将允许执行这些脚本。请注意,某些浏览器会nonce在检查页面源时隐藏该属性。
陷阱
在strict-dynamic
您的 CSP 中,您只需将 nonce 添加到<script>
初始 HTML 响应中存在的标签。'strict-dynamic'允许执行动态添加到页面的脚本,只要它们是由安全的、已受信任的脚本加载的(请参阅规范)。
选项 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}'
。
警告
占位{HASHED_INLINE_SCRIPT}
符必须替换为内联脚本的 base64 编码的 SHA-256 哈希值,该内联脚本可用于加载其他脚本(请参阅下一节)。您可以<script>
使用此工具计算静态内联块的 SHA 哈希值。另一种方法是在 Chrome 开发者控制台中检查 CSP 违规警告,其中包含被阻止脚本的哈希值,并将这些哈希值作为“sha256-...”添加到策略中。攻击者注入的脚本将被浏览器阻止,因为浏览器只允许执行哈希内联脚本及其动态添加的任何脚本。
动态加载源脚本
所有来自外部的脚本都需要通过内联脚本动态加载,因为跨浏览器仅支持内联脚本的 CSP 哈希值(来源脚本的哈希值在浏览器之间并未得到很好的支持)。
被 CSP 阻止
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
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>
标记之间的空格字符很重要。您可以使用此工具计算内联脚本的 CSP 哈希值。
脚本加载注意事项
在上面的代码片段中,s.async = false
添加 是为了确保 foo 在 bar 之前执行(即使 bar 首先加载)。在此代码片段中,s.async = false
在脚本加载时不会阻止解析器;这是因为脚本是动态添加的。解析器只会在脚本执行时停止,就像它对async
脚本的行为一样。然而,对于这个片段,请记住:
- 一个/两个脚本可能会在文档下载完成之前执行。如果您希望文档在脚本执行时准备就绪,则需要在附加脚本之前等待该
DOMContentLoaded
事件。如果这导致性能问题(因为脚本没有足够早地开始下载),您可以在页面的较早位置使用预加载标记。 defer = true
不会做任何事。如果您需要这种行为,则必须在想要运行脚本时手动运行该脚本。
第3 步:重构 HTML 模板和客户端代码以删除与 CSP 不兼容的模式
内联事件处理程序(例如onclick="…", onerror="…")和 JavaScript URI ( <a href="javascript:…">
) 可用于运行脚本。这意味着发现 XSS 漏洞的攻击者可以注入这种 HTML 并执行恶意 JavaScript。基于随机数或散列的 CSP 不允许使用此类标记。如果您的站点使用上述任何模式,则需要将它们重构为更安全的替代方案。
如果您在上一步中启用了 CSP,则每次 CSP 阻止不兼容的模式时,您都可以在控制台中看到 CSP 违规。
在大多数情况下,修复很简单:
要重构内联事件处理程序,请重写它们以从 JavaScript 块添加
被 CSP 阻止
<span onclick="doThings();">A thing.</span>
CSP 将阻止内联事件处理程序。
CSP 允许
<span id="things">A thing.</span>
<script nonce="${nonce}">
document.getElementById('things')
.addEventListener('click', doThings);
</script>
CSP 将允许通过 JavaScript 注册事件处理程序。
对于javascript:
URI,您可以使用类似的模式
被 CSP 阻止
<a href="javascript:linkClicked()">foo</a>
CSP 将阻止 javascript: URI。
CSP 允许
<a id="foo">foo</a>
<script nonce="${nonce}">
document.getElementById('foo')
.addEventListener('click', linkClicked);
</script>
CSP 将允许通过 JavaScript 注册事件处理程序。
eval()
在 JavaScript 中使用
如果您的应用程序用于eval()
将 JSON 字符串序列化转换为 JS 对象,您应该将此类实例重构为JSON.parse()
,这也更快。
如果您无法删除 的所有使用eval()
,您仍然可以设置严格的基于随机数的 CSP,但您必须使用'unsafe-eval'
CSP 关键字,这将使您的策略安全性稍差。
https://glitch.com/edit/#!/strict-csp-codelab?path=demo%2Fsolution_nonce_csp.html%3A1%3A0
第 4 步:添加回退以支持 Safari 和旧版浏览器
如果您需要支持早于上面列出的浏览器版本:
- 使用
'strict-dynamic'
需要添加https:
作为旧版本 Safari 的后备。通过这样做:- 所有支持的浏览器
'strict-dynamic'
都会忽略https:
回退,因此这不会降低策略的强度。 - 在旧浏览器中,只有来自 HTTPS 源的外部脚本才允许加载。
javascript:
这比严格的 CSP 安全性较低(这是一种后备),但仍会防止某些常见的 XSS 原因,例如URI注入,因为'unsafe-inline'
在存在哈希或随机数时不存在或被忽略。
- 所有支持的浏览器
- 为了确保与非常旧的浏览器版本(4 年以上)的兼容性,您可以添加
'unsafe-inline'
作为后备。所有最新的浏览器都会忽略'unsafe-inline'
CSP 随机数或哈希值是否存在。
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
TIP
https:
并且unsafe-inline
不要降低您的策略的安全性,因为所有支持strict-dynamic
.
第 5 步:部署 CSP
在确认本地开发环境中的 CSP 没有阻止任何合法脚本后,您可以继续将 CSP 部署到您的(暂存,然后)生产环境:
- (可选)使用标头以仅报告模式部署您的 CSP Content-Security-Policy-Report-Only。了解有关报告 API的更多信息。在实际执行 CSP 限制之前,仅报告模式可以方便地测试潜在的重大更改,例如生产中的新 CSP。在仅报告模式下,您的 CSP 不会影响您的应用程序的行为(实际上不会中断)。但是,当遇到与 CSP 不兼容的模式时,浏览器仍会生成控制台错误和违规报告(因此您可以看到最终用户会遇到什么问题)。
- 一旦您确信您的 CSP 不会导致最终用户损坏,请使用Content-Security-Policy响应标头部署您的 CSP。只有完成此步骤后,CSP 才会开始保护您的应用程序免受 XSS 攻击。通过 HTTP 标头服务器端设置 CSP 比将其设置为
<meta>
标签更安全;如果可以,请使用标题。
陷阱
通过使用CSP 评估器或 Lighthouse进行检查,确保您使用的 CSP 是“严格的” 。这非常重要,因为即使是对策略的微小更改也会显着降低其安全性。
警告
为生产流量启用 CSP 时,由于浏览器扩展和恶意软件,您可能会在 CSP 违规报告中看到一些噪音。
限制
一般来说,严格的 CSP 提供了一个强大的附加安全层,有助于缓解 XSS。在大多数情况下,CSP 显着减少了攻击面(javascript:URI 等危险模式被完全关闭)。但是,根据您使用的 CSP 的类型(随机数、散列、带或不带'strict-dynamic'),在某些情况下 CSP 不保护:
- 如果你是一个脚本,但是直接注入到正文或该元素的src参数中。
<script>
- 如果有注入到动态创建的脚本 ( ) 的位置,包括任何基于参数值document.createElement('script')创建 DOM 节点的库函数。script这包括一些常见的 API,例如 jQuery.html()以及jQuery < 3.0中的.get()和。.post()
- 如果旧 AngularJS 应用程序中存在模板注入。可以注入 AngularJS 模板的攻击者可以使用它来执行任意 JavaScript。
- 如果策略包含、'unsafe-eval'注入和其他一些很少使用的 API。eval()setTimeout()
开发人员和安全工程师在代码审查和安全审计期间应特别注意此类模式。[您可以在此 CSP 演示文稿](https://static.sched.com/hosted_files/locomocosec2019/db/CSP - A Successful Mess Between Hardening and Mitigation (1).pdf#page=27)中找到有关上述案例的更多详细信息。
可信类型很好地补充了严格的 CSP,并且可以有效地防止上面列出的一些限制。在 web.dev了解有关如何使用可信类型的更多信息。