内容安全策略(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-xxx3.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-requests4.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-moduleDjango 配置
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-ancestors、report-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: SAMEORIGIN8.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 应该使用哪个?
回答:
| 特性 | Nonce | Hash |
|---|---|---|
| 动态内容 | ✅ 适合 | ❌ 不适合 |
| 静态内容 | ✅ 适合 | ✅ 适合 |
| 实现复杂度 | 中 | 低 |
| 性能 | 每次请求生成 | 预先计算 |
推荐:
- 动态内容使用 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 等)形成纵深防御体系。