文章目录
- [使用 `nonce` 和 `hash` 方案](#使用
nonce和hash方案) -
- [3. 关于 `strict-dynamic` 及其他高级指令](#3. 关于
strict-dynamic及其他高级指令) - [5. 实际配置示例](#5. 实际配置示例)
-
- [示例1:使用 nonce 允许特定内联脚本](#示例1:使用 nonce 允许特定内联脚本)
- [示例2:使用 hash 允许特定内联脚本](#示例2:使用 hash 允许特定内联脚本)
- [示例3:结合 `strict-dynamic` 和 nonce](#示例3:结合
strict-dynamic和 nonce)
- [6. 总结建议](#6. 总结建议)
- [使用 strict-dynamic 方案](#使用 strict-dynamic 方案)
-
-
- [1. `strict-dynamic` 是什么?核心原理是什么?](#1.
strict-dynamic是什么?核心原理是什么?) - [2. 如何一步步实现 `strict-dynamic` 模式?](#2. 如何一步步实现
strict-dynamic模式?) -
- 第一步:为每个页面请求生成一个唯一的随机数 (Nonce)
- [第二步:设置包含 `nonce` 和 `strict-dynamic` 的 CSP 响应头](#第二步:设置包含
nonce和strict-dynamic的 CSP 响应头) - [第三步:为所有初始的 `<script>` 标签添加 `nonce` 属性](#第三步:为所有初始的
<script>标签添加nonce属性)
- [3. 实施过程中的重要陷阱与注意事项](#3. 实施过程中的重要陷阱与注意事项)
- [4. 一个完整的实施示例](#4. 一个完整的实施示例)
- 总结
- [1. `strict-dynamic` 是什么?核心原理是什么?](#1.
-
使用 nonce 和 hash 方案
CSP 提供了两种精确控制内联脚本/样式的方法。
随机数(nonce)
- 原理 :服务器在响应时生成一个一次性的随机值 (nonce),将其添加到 CSP 头的
script-src中,同时在需要允许执行的<script>标签上设置相同的nonce属性。浏览器只执行 nonce 匹配的脚本。 - 安全性:攻击者无法提前知道这个随机数,因此无法注入能被执行的脚本。
- 实现步骤 :
-
后端在每次响应页面时,生成一个足够随机的字符串(例如使用安全的随机数生成器,长度至少 128 位)。
-
在 HTTP 响应头中添加:
Content-Security-Policy: script-src 'nonce-随机值' -
在 HTML 中需要允许的
<script>标签上添加nonce="随机值"属性。例如:html<script nonce="随机值"> console.log('这个脚本会执行'); </script> -
注意:外部脚本也可以通过 nonce 来允许,但更常见的是通过域名白名单。
-
哈希值(hash)
- 原理 :计算内联脚本内容的哈希值(如 SHA-256),然后将该哈希值加入 CSP 头的
script-src中。浏览器会计算每个内联脚本的哈希,只有匹配的才执行。 - 适用场景:适合静态的内联脚本(内容不会动态变化)。
- 实现步骤 :
-
确定需要允许的内联脚本内容,例如:
html<script>alert('Hello');</script> -
计算该内容的哈希值(注意不要包含
<script>标签本身,只计算里面的代码)。可以使用在线工具或命令行:bashecho -n "alert('Hello');" | openssl dgst -sha256 -binary | base64 -
将哈希值加入 CSP 头,格式为
'sha256-哈希值':Content-Security-Policy: script-src 'sha256-计算出的哈希值' -
浏览器会自动匹配内联脚本,无需在标签上添加额外属性。
-
3. 关于 strict-dynamic 及其他高级指令
strict-dynamic 是一种用于进一步强化 CSP 的指令,通常与 nonce 配合使用。它的作用是:允许已经通过 nonce 或 hash 信任的脚本动态加载的其他脚本(例如通过 appendChild 添加的 <script> 标签)也自动被信任,而无需显式列出所有来源的域名。
- 示例:
script-src 'nonce-abc' 'strict-dynamic'表示:只要初始脚本带有 nonce,那么该脚本内通过document.createElement('script')创建的脚本也会被允许执行(尽管它们可能没有 nonce)。这可以简化对动态加载脚本的管理,同时保持安全性。
5. 实际配置示例
示例1:使用 nonce 允许特定内联脚本
后端生成随机数(如 Rnd123)并设置响应头:
Content-Security-Policy: script-src 'nonce-Rnd123'
HTML 中:
html
<script nonce="Rnd123">
console.log('允许执行');
</script>
<script>
console.log('这个没有 nonce,会被阻止');
</script>
示例2:使用 hash 允许特定内联脚本
计算脚本 console.log('safe'); 的 SHA-256 哈希值为 xyz123...,设置头:
Content-Security-Policy: script-src 'sha256-xyz123...'
HTML 中:
html
<script>console.log('safe');</script> <!-- 允许 -->
<script>console.log('unsafe');</script> <!-- 阻止 -->
示例3:结合 strict-dynamic 和 nonce
Content-Security-Policy: script-src 'nonce-Rnd123' 'strict-dynamic' https: // 注意:strict-dynamic 会覆盖白名单
这样,带有 nonce 的脚本中动态创建的脚本也会被允许。
6. 总结建议
- 对于新项目,尽量将所有脚本移到外部文件,通过
'self'或具体域名来允许,避免内联脚本。 - 如果确实需要内联脚本,优先使用 nonce 或 hash ,而不是
unsafe-inline。 - 避免使用
unsafe-eval,如果必须,考虑使用'unsafe-eval'并配合其他限制,但风险自担。 - 对于样式,同样使用 nonce/hash 控制内联样式,尽量避免
style属性(因为它们无法用 nonce/hash 控制,只能靠unsafe-inline允许,或者完全禁止)。
使用 strict-dynamic 方案
我们来详细探讨一下 strict-dynamic 模式。这是一个非常强大的 CSP 特性,能显著提升网站安全性,但理解其原理和正确实施至关重要。
可以把 strict-dynamic 理解为一种 信任的传递机制。它抛弃了传统的域名白名单思路,转而采用基于信任链的脚本加载策略。
1. strict-dynamic 是什么?核心原理是什么?
传统的 CSP 策略通常依赖 'self' 或 https://trusted.com 这样的域名白名单来允许脚本加载。但这存在风险:如果白名单域名内存在可以被攻击者利用的 JSONP 接口或可以上传文件的漏洞,攻击者就可能绕过 CSP 。
strict-dynamic 就是为了解决这个问题而设计的 CSP 3 级规范。它的核心思想是:不再信任特定的域名,而是信任特定的脚本本身 。
它的工作原理如下:
- 必须与
nonce或hash配合 :strict-dynamic自身无法独立工作。你必须使用'nonce-...'或'...-hash...'来明确标记出哪些最初的脚本是可信的 。 - 信任的传递 :当一个被
nonce或hash标记为可信的脚本在页面中执行时,如果它通过合法的 DOM API(例如document.createElement('script'))动态地创建并插入新的脚本标签到页面中,那么这些后来动态创建的脚本也会自动获得信任并被允许执行 。 - 禁用域名白名单 :一旦在
script-src中使用了'strict-dynamic',现代浏览器会自动忽略该指令中同时列出的其他基于域名的白名单规则(例如'self'或https://example.com)。这意味着你的策略变得极其精简,完全围绕信任链展开。
简单来说 :你告诉浏览器"我信任这个带有特定 nonce 的脚本",然后浏览器就会说:"好的,不仅这个脚本可以执行,它之后创建的任何兄弟脚本,我也都信任。"
2. 如何一步步实现 strict-dynamic 模式?
实施 strict-dynamic 模式主要分三步:生成 nonce、配置 CSP 头、为脚本添加 nonce 属性。
第一步:为每个页面请求生成一个唯一的随机数 (Nonce)
这一步必须在服务器端 完成。你需要确保每个页面响应都拥有一个独一无二、不可预测的随机字符串。这个字符串就是你的 nonce 值 。
- 实现方式 :在你的后端代码中(例如中间件、过滤器或控制器),为每个请求生成一个安全的随机数。例如,在 Next.js 的中间件中,可以使用
crypto.randomUUID()来生成 。在 Rails 中,可以使用SecureRandom.base64(16)。 - 关键点:这个值必须是真的随机且每次请求都不同,绝不能是固定的。
第二步:设置包含 nonce 和 strict-dynamic 的 CSP 响应头
在你生成了 nonce 之后,将其嵌入到 HTTP 响应的 Content-Security-Policy 头中。
-
CSP 头格式 :
Content-Security-Policy: script-src 'nonce-你生成的随机值' 'strict-dynamic' [可选后备] -
示例 :
Content-Security-Policy: script-src 'nonce-abc123def456' 'strict-dynamic' -
关于后备方案 :由于极少数非常老的浏览器可能不支持
strict-dynamic,你可以在它后面加上https:或'self'作为后备策略。但请注意,支持strict-dynamic的浏览器会自动忽略这些后备规则 。
第三步:为所有初始的 <script> 标签添加 nonce 属性
在你的 HTML 响应中,你需要为每一个不是由其他可信脚本动态创建 的 <script> 标签,都添加上 nonce 属性,其值必须与 HTTP 头中的 nonce 完全一致 。
-
外部脚本 :
html<script src="https://your-cdn.com/main.js" nonce="abc123def456"></script> -
内联脚本 :
html<script nonce="abc123def456"> console.log('这个内联脚本也会被执行'); </script>
这样一来,所有没有 nonce 属性或 nonce 值错误的 <script> 标签都会被浏览器阻止。而 main.js 中通过合法方式动态添加的任何脚本,即使它们没有 nonce 属性,也会因为 strict-dynamic 的信任传递机制而被允许 。
3. 实施过程中的重要陷阱与注意事项
-
性能考量:动态渲染
使用
nonce策略通常意味着你需要采用动态渲染 页面,而不是静态生成(SSG)。因为nonce必须在请求时生成并注入到 HTML 中,所以每个请求都需要实时渲染页面。这会增加服务器负载,并可能影响 CDN 缓存 。 -
样式表 (
style-src) 策略
strict-dynamic只适用于script-src指令 。对于样式,你仍然需要单独配置style-src。如果你的应用或第三方脚本需要动态地向页面注入内联样式,你可能会遇到 CSP 错误。解决方式要么是放宽style-src策略(例如添加'unsafe-inline'),要么依赖第三方库是否支持通过nonce传递样式 。目前,处理动态添加的内联样式仍然是 CSP 实施中的一个难点。 -
与第三方脚本的兼容性
许多第三方脚本(如分析工具、聊天挂件)会动态加载额外的脚本。
strict-dynamic模式天然地支持这种行为,只要初始的第三方脚本标签带有正确的nonce,它动态加载的其他脚本通常也能正常工作。但如前所述,要注意它们可能带来的样式问题 。一些设计良好的第三方库也提供了在初始化时传递nonce的选项,以更好地配合 CSP 。 -
潜在的安全绕过风险
尽管
strict-dynamic非常安全,但它并非万无一失。如果页面中已经加载了某些特定的、被广泛使用的 JavaScript 库(如老版本的require.js),攻击者可能会利用这些库的特性,在不需要正确nonce的情况下执行恶意代码。这被称为"Script gadgets"攻击 。因此,保持所有前端依赖库的最新版本同样重要。
4. 一个完整的实施示例
假设你使用 Node.js + Express 框架。
后端 (Server - e.g., with Express):
javascript
import crypto from 'crypto';
app.use((req, res, next) => {
// 1. 为每个请求生成一个安全的 base64 编码的 nonce
const nonce = crypto.randomBytes(16).toString('base64');
// 2. 构建 CSP 字符串,将 nonce 嵌入
const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
// 3. 设置 CSP 响应头
res.setHeader('Content-Security-Policy', csp);
// 4. 将 nonce 存储起来,以便后续在渲染 HTML 模板时使用
res.locals.nonce = nonce;
next();
});
// ... 在你的路由或模板渲染逻辑中 ...
app.get('/', (req, res) => {
// 假设你使用模板引擎,将 nonce 传递给模板
res.render('index', { nonce: res.locals.nonce });
});
前端 (HTML 模板 - e.g., with EJS):
html
<!DOCTYPE html>
<html>
<head>
<title>Strict-Dynamic Demo</title>
</head>
<body>
<h1>Hello, CSP with strict-dynamic!</h1>
<!-- 这个脚本因为有正确的 nonce 而会被加载和执行 -->
<script src="/js/app.js" nonce="<%= nonce %>"></script>
<!-- 这个内联脚本也会被执行 -->
<script nonce="<%= nonce %>">
console.log('Inline script with nonce ran.');
</script>
<!-- 这个脚本没有 nonce,会被浏览器阻止 -->
<script src="/js/evil.js"></script>
<!-- 假设在 app.js 中,它执行了以下代码:
var newScript = document.createElement('script');
newScript.src = '/js/dynamic.js';
document.body.appendChild(newScript);
那么 dynamic.js 会因为信任传递机制而被允许加载。
-->
</body>
</html>
总结
strict-dynamic 模式将 CSP 从基于域名的粗粒度管控,升级为基于信任链的精确定向策略。它能有效防御因 CDN 被攻破或域名白名单内存在漏洞而引发的 XSS 攻击。尽管实施它需要后端配合动态渲染,并需留意样式策略和少数已知的绕过风险,但它无疑是目前最值得推荐的现代 CSP 配置方案之一。
#你提到的这些概念确实是配置内容安全策略(CSP)时的核心,但网上的说法可能有些混乱,甚至存在错误。我来逐一解释这些术语,并说明如何正确实现它们。