{T}

内容安全策略(CSP)

CSP(Content Security Policy)是一种强大的 HTTP 安全头,用于防止 XSS、数据注入等攻击,是现代 Web 安全的重要防线。

一、CSP 概述

1.1 什么是 CSP

内容安全策略(Content Security Policy,简称 CSP)是一种声明式机制,让 Web 开发者可以精确控制页面能够加载哪些资源。通过指定允许的资源来源,CSP 可以有效防止 XSS 攻击、数据泄露等安全问题。

1.2 CSP 的工作原理

code
┌──────────────┐                    ┌──────────────┐
│   浏览器      │  ← HTTP 响应头 ──  │   服务器      │
└──────────────┘                    └──────────────┘
       │
       ↓ 解析 CSP 头
┌──────────────────────────────────────────────────┐
│  Content-Security-Policy: default-src 'self'     │
│                   ↓                              │
│  ┌────────────────────────────────────────────┐ │
│  │              资源加载策略                   │ │
│  │  • 脚本:只允许同源                         │ │
│  │  • 样式:只允许同源                         │ │
│  │  • 图片:只允许同源                         │ │
│  │  • 连接:只允许同源                         │ │
│  └────────────────────────────────────────────┘ │
       │
       ↓ 加载资源时检查
┌──────────────┐                    ┌──────────────┐
│  允许加载     │  或  违规报告 ──→  │  阻止加载     │
└──────────────┘                    └──────────────┘

1.3 CSP 的主要作用

功能说明防护目标
限制资源来源只允许加载指定来源的资源XSS、数据泄露
禁止内联脚本阻止 <script> 内联代码执行XSS
禁止 eval阻止动态代码执行XSS、代码注入
限制表单提交控制表单提交目标数据泄露
限制嵌入控制页面被嵌入行为点击劫持
违规报告自动报告策略违规安全监控

// ... 中间省略 ...

| 协议 | 特定协议所有资源 | https: data: blob: |

3.3 Nonce 和 Hash

Nonce(一次性令牌)

code
Content-Security-Policy: script-src 'self' 'nonce-abc123randomtoken'
html
<!-- 服务端生成的 nonce 必须匹配 -->
<script nonce="abc123randomtoken">
  console.log('安全的内联脚本');
</script>

Hash(内容哈希)

http
Content-Security-Policy: script-src 'self' 'sha256-jzgBGA4UWFFmpOBq0JpdsySukE1FrEN5bUpoK8Z29fY='
html
<!-- 脚本内容必须精确匹配 -->
<script>
  console.log('这个内容会被 hash 验证');
</script>

生成 Hash 的方法

javascript
// Node.js 生成 SHA256 Hash
const crypto = require('crypto');

function generateCspHash(content) {
  const hash = crypto
    .createHash('sha256')
    .update(content)
    .digest('base64');
  
  return `sha256-${hash}`;
}

const scriptContent = 'console.log("Hello")';
const hash = generateCspHash(scriptContent);
console.log(hash); // sha256-xxx

3.4 来源配置对比

http
# 最严格(推荐)
script-src 'self';

# 允许特定 CDN
script-src 'self' https://cdn.example.com;

# 使用 nonce 处理内联脚本
script-src 'self' 'nonce-abc123';

# 使用 strict-dynamic(现代浏览器)
script-src 'strict-dynamic' 'nonce-abc123';

# 不推荐:使用 unsafe-inline(降低安全性)
script-src 'self' 'unsafe-inline';

# 不推荐:使用 unsafe-eval(大幅降低安全性)
script-src 'self' 'unsafe-inline' 'unsafe-eval';

四、CSP 配置实战

4.1 基础配置模板

最小配置

http
Content-Security-Policy: default-src 'self'

中等安全配置

http
Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  font-src 'self';
  object-src 'none';
  frame-ancestors 'self';
  base-uri 'self';
  form-action 'self'

高安全配置

http
Content-Security-Policy:
  default-src 'none';
  script-src 'self' 'nonce-${NONCE}';
  style-src 'self' 'nonce-${NONCE}';
  img-src 'self' data:;
  connect-src 'self';
  font-src 'self';
  object-src 'none';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  upgrade-insecure-requests

4.2 服务端配置示例

Express.js + Helmet

javascript
const express = require('express');
const helmet = require('helmet');
const crypto = require('crypto');

const app = express();

// Nonce 中间件
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});


  // ... 中间省略 ...

        console.log('安全的内联脚本');
      </script>
    </body>
    </html>
  `);
});

Nginx 配置

nginx
server {
    listen 443 ssl http2;
    server_name example.com;

    # CSP 配置
    set $csp "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; font-src 'self'; object-src 'none'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'";
    
    add_header Content-Security-Policy $csp always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # 静态文件
    location / {
        root /var/www/html;
        try_files $uri $uri/ =404;
    }

    # API 代理
    location /api {
        proxy_pass http://backend:3000;
    }
}

# 使用 Lua 动态生成 nonce(高级配置)
# 需要 nginx + lua-nginx-module

Django 配置

python
# settings.py

# django-csp 配置
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "https://cdn.example.com")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_CONNECT_SRC = ("'self'", "https://api.example.com")
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com")
CSP_OBJECT_SRC = ("'none'",)
CSP_FRAME_ANCESTORS = ("'self'",)
CSP_BASE_URI = ("'self'",)
CSP_FORM_ACTION = ("'self'",)

# 报告模式(测试阶段)
CSP_REPORT_ONLY = False
CSP_REPORT_URI = "/csp-report/"

# 中间件
MIDDLEWARE = [
    # ...
    'csp.middleware.CSPMiddleware',
    # ...
]

4.3 HTML Meta 标签配置

html
<!-- HTML 结构省略,仅展示关键 JS 逻辑 -->

注意:Meta 标签方式有局限性:

  • 不支持 frame-ancestorsreport-uri 等指令
  • 无法动态生成 nonce
  • 建议仅在无法修改 HTTP 头时使用

五、处理内联脚本和样式

5.1 为什么需要处理内联代码

html
<!-- ❌ 这些内联代码会被 CSP 阻止 -->
<script>
  console.log('内联脚本');
</script>

<div onclick="alert('事件处理器')">点击</div>

<style>
  .content { color: red; }
</style>

5.2 使用 Nonce 处理

javascript
// 服务端生成 nonce
const crypto = require('crypto');
const nonce = crypto.randomBytes(16).toString('base64');

// 设置 CSP 头
res.setHeader(
  'Content-Security-Policy',
  `script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'`
);

// 模板中使用
res.render('page', { nonce });
html
<!-- HTML 模板 -->
<script nonce="<%= nonce %>">
  console.log('安全的内联脚本');
</script>

<style nonce="<%= nonce %>">
  .content { color: red; }
</style>

5.3 使用 Hash 处理

javascript
// 预先计算 hash
const scriptHash = 'sha256-jzgBGA4UWFFmpOBq0JpdsySukE1FrEN5bUpoK8Z29fY=';

// 设置 CSP 头
res.setHeader(
  'Content-Security-Policy',
  `script-src 'self' '${scriptHash}'`
);

5.4 使用 strict-dynamic

http
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-abc123'
html
<script nonce="abc123">
  // 动态创建的脚本会继承 nonce 的信任
  const script = document.createElement('script');
  script.src = 'https://cdn.example.com/script.js';
  document.head.appendChild(script);
</script>

5.5 重构内联代码

html
<!-- ❌ 不推荐:内联事件处理器 -->
<button onclick="handleClick()">点击</button>

<!-- ✅ 推荐:使用 JavaScript 绑定事件 -->
<button id="myButton">点击</button>

<script src="/js/app.js"></script>
javascript
// /js/app.js
document.getElementById('myButton').addEventListener('click', handleClick);

六、CSP 违规报告

6.1 配置报告端点

http
# 使用 report-uri(兼容性好)
Content-Security-Policy: default-src 'self'; report-uri /csp-report

# 使用 report-to(推荐,支持 Reporting API)
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-report"}]}

6.2 服务端接收报告

javascript
// Express.js 接收 CSP 报告
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body['csp-report'];
  
  console.log('CSP 违规报告:');
  console.log('- 文档 URL:', report['document-uri']);
  console.log('- 违规指令:', report['violated-directive']);
  console.log('- 阻止的 URI:', report['blocked-uri']);
  console.log('- 原始策略:', report['original-policy']);
  console.log('- 来源文件:', report['source-file']);
  console.log('- 行号:', report['line-number']);
  console.log('- 列号:', report['column-number']);
  
  // 存储到数据库或发送到监控系统
  // saveCspReport(report);
  
  res.status(204).end();
});

6.3 报告数据结构

json
{
  "csp-report": {
    "document-uri": "https://example.com/page",
    "referrer": "https://example.com/",
    "violated-directive": "script-src-elem",
    "effective-directive": "script-src-elem",
    "original-policy": "default-src 'self'; script-src 'self'; report-uri /csp-report",
    "disposition": "enforce",
    "blocked-uri": "https://evil.com/malicious.js",
    "line-number": 15,
    "column-number": 5,
    "source-file": "https://example.com/page",
    "status-code": 200,
    "script-sample": "alert(1)"
  }
}

6.4 报告分析工具

javascript
/**
 * CSP 报告分析器
 */
class CspReportAnalyzer {
  constructor() {
    this.reports = [];
  }

  /**
   * 添加报告
   */
  addReport(report) {

  // ... 中间省略 ...


    return recommendations;
  }
}

const analyzer = new CspReportAnalyzer();

七、CSP 部署策略

7.1 渐进式部署流程

code
阶段 1: Report-Only 模式
├── 配置 Content-Security-Policy-Report-Only
├── 收集违规报告
├── 分析并修复问题
└── 验证不影响功能

        ↓

阶段 2: 宽松策略
├── 实施 base-uri、form-action 等低风险指令
├── 使用 unsafe-inline(临时)
├── 持续监控报告
└── 验证核心功能正常

        ↓

阶段 3: 逐步收紧
├── 移除 unsafe-inline,使用 nonce/hash
├── 添加 script-src、style-src 限制
├── 收紧其他资源限制
└── 持续监控

        ↓

阶段 4: 严格策略
├── 实施完整 CSP
├── 启用违规报告
├── 定期审计策略
└── 持续优化

7.2 Report-Only 模式

javascript
// 仅报告模式(不阻止违规)
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      reportUri: ['/csp-report']
    },
    reportOnly: true  // 仅报告,不阻止
  })
);

7.3 实际部署案例

电商网站 CSP 配置

javascript
// 电商网站 CSP 配置示例
const ecomCspConfig = {
  directives: {
    // 默认策略
    defaultSrc: ["'self'"],
    
    // 脚本:允许 Google Analytics、支付网关
    scriptSrc: [
      "'self'",
      'https://www.googletagmanager.com',
      'https://ssl.google-analytics.com',
      'https://js.stripe.com',

  // ... 中间省略 ...

    formAction: ["'self'"],
    frameAncestors: ["'self'"]
  }
};

app.use(helmet.contentSecurityPolicy(ecomCspConfig));

单页应用(SPA)CSP 配置

javascript
// React SPA CSP 配置
const spaCspConfig = {
  directives: {
    defaultSrc: ["'self'"],
    
    // 脚本:CDN + nonce
    scriptSrc: [
      "'self'",
      'https://cdn.example.com',
      (req, res) => `'nonce-${res.locals.nonce}'`
    ],
    

  // ... 中间省略 ...

    // 其他
    objectSrc: ["'none'"],
    baseUri: ["'self'"],
    frameAncestors: ["'none'"]
  }
};

八、CSP 与其他安全措施的关系

8.1 CSP 与 XSS 防护

code
┌─────────────────────────────────────────────────────────┐
│                   XSS 防护层次                           │
├─────────────────────────────────────────────────────────┤
│  第1层:输入验证                                        │
│  ├─ 白名单验证                                          │
│  └─ 格式验证                                            │
├─────────────────────────────────────────────────────────┤
│  第2层:输出编码                                        │
│  ├─ HTML 实体编码                                       │
│  └─ URL 编码                                            │
├─────────────────────────────────────────────────────────┤
│  第3层:CSP(本节重点)                                 │
│  ├─ 限制脚本来源                                        │
│  ├─ 禁止内联脚本                                        │
│  └─ 禁止 eval                                           │
├─────────────────────────────────────────────────────────┤
│  第4层:HttpOnly Cookie                                 │
│  └─ 防止 JavaScript 读取敏感 Cookie                    │
└─────────────────────────────────────────────────────────┘

CSP 作为 XSS 防护的最后一道防线

  • 即使输入验证和输出编码失效,CSP 仍能阻止恶意脚本执行
  • CSP 不是 XSS 防护的替代品,而是补充

8.2 CSP 与点击劫持防护

http
# CSP frame-ancestors 替代 X-Frame-Options
Content-Security-Policy: frame-ancestors 'self' https://trusted.com

# 同时使用 X-Frame-Options(兼容性)
X-Frame-Options: SAMEORIGIN

8.3 CSP 与 CSRF 防护

http
# CSP form-action 限制表单提交目标
Content-Security-Policy: form-action 'self'

# 配合 CSRF Token 使用
# form-action 限制提交目标,CSRF Token 验证请求来源

8.4 综合安全配置

javascript
// 综合安全配置
app.use(
  // CSP
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],

  // ... 中间省略 ...

    preload: true
  }),
  
  // CSRF 保护
  csrf({ cookie: true })
);

九、常见问题与解决方案

9.1 常见错误处理

错误 1:内联脚本被阻止

code
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.

解决方案

javascript
// 方案1:使用 nonce
script-src 'self' 'nonce-abc123';

// 方案2:使用 hash
script-src 'self' 'sha256-xxx';

// 方案3:重构代码(推荐)
// 将内联脚本移到外部文件

错误 2:事件处理器被阻止

code
Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self'".

解决方案

html
<!-- ❌ 不推荐 -->
<button onclick="handleClick()">点击</button>

<!-- ✅ 推荐 -->
<button id="btn">点击</button>
<script>
  document.getElementById('btn').addEventListener('click', handleClick);
</script>

错误 3:eval 被阻止

code
Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self'".

解决方案

javascript
// ❌ 不推荐:使用 eval
eval(code);
new Function(code);
setTimeout(code, 100);

// ✅ 推荐:重构代码
// 如果必须使用,添加 'unsafe-eval'(不推荐)
script-src 'self' 'unsafe-eval';

错误 4:第三方脚本被阻止

code
Refused to load the script 'https://example.com/script.js' because it violates the following Content Security Policy directive: "script-src 'self'".

解决方案

http
# 添加第三方域名到白名单
script-src 'self' https://example.com;

# 或使用 strict-dynamic(推荐)
script-src 'strict-dynamic' 'nonce-abc123';

9.2 调试技巧

javascript
// 1. 使用 Report-Only 模式测试
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

// 2. 浏览器控制台查看违规
// 打开开发者工具 → Console 查看 CSP 错误

// 3. 使用 CSP Evaluator 工具
// https://csp-evaluator.withgoogle.com/

// 4. 添加 report-sample 获取违规代码样本
script-src 'self' 'report-sample';

十、常见问题解答(FAQ)

Q1: CSP 会影响网站性能吗?

回答:影响很小,主要是:

  • 初始配置需要时间
  • 违规报告产生网络请求
  • 浏览器需要检查资源来源

建议:使用 Report-Only 模式先行测试。

Q2: 如何处理第三方嵌入(如 Google Analytics)?

回答:将第三方域名添加到对应的 CSP 指令:

http
# Google Analytics
script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com;
img-src 'self' https://www.google-analytics.com;

# Google Fonts
font-src 'self' https://fonts.gstatic.com;
style-src 'self' https://fonts.googleapis.com;

# Stripe
script-src 'self' https://js.stripe.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com;
connect-src 'self' https://api.stripe.com;

Q3: CSP 能完全防止 XSS 吗?

回答:不能完全防止,但能大幅降低风险:

场景CSP 是否有效
外部注入的恶意脚本✅ 有效
内联脚本注入✅ 有效(配置正确时)
DOM 型 XSS(基于用户输入)⚠️ 部分有效
服务端 XSS(返回恶意内容)⚠️ 部分有效

建议:CSP 应与其他防护措施配合使用。

Q4: 如何在开发环境和生产环境使用不同策略?

回答

javascript
const isProduction = process.env.NODE_ENV === 'production';

const cspDirectives = {
  defaultSrc: ["'self'"],
  scriptSrc: isProduction 
    ? ["'self'", 'https://cdn.example.com']  // 生产环境
    : ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // 开发环境
  // ...
};

app.use(helmet.contentSecurityPolicy({ directives: cspDirectives }));

Q5: Nonce 和 Hash 应该使用哪个?

回答

特性NonceHash
动态内容✅ 适合❌ 不适合
静态内容✅ 适合✅ 适合
实现复杂度
性能每次请求生成预先计算

推荐

  • 动态内容使用 Nonce
  • 静态内联代码使用 Hash
  • 大型项目使用 strict-dynamic

Q6: 如何处理 WebSocket 连接?

回答:使用 connect-src 指令:

http
# 允许 WebSocket 连接
connect-src 'self' wss://ws.example.com;

# 允许所有同源 WebSocket
connect-src 'self' ws: wss:;

Q7: CSP 对 SEO 有影响吗?

回答:正确配置的 CSP 对 SEO 没有负面影响:

  • 确保所有必要资源都能加载
  • 避免阻止搜索引擎爬虫需要的脚本
  • 使用 Report-Only 模式测试

十一、安全检测清单

开发阶段

  • 使用 Report-Only 模式测试
  • 分析违规报告
  • 重构内联代码
  • 移除 eval 使用

测试阶段

  • 验证所有功能正常
  • 测试第三方集成
  • 检查浏览器控制台错误
  • 使用 CSP Evaluator 审计

部署阶段

  • 实施正式 CSP 策略
  • 配置违规报告
  • 监控报告数据
  • 定期审计和优化

十二、参考资料


💡 提示:CSP 是防御 XSS 的重要手段,但不是万能的。推荐使用渐进式部署策略,从 Report-Only 模式开始,逐步收紧策略。同时应配合其他安全措施(如输入验证、输出编码、HttpOnly Cookie 等)形成纵深防御体系。