CSP(Content Security Policy)是一种强大的 HTTP 安全头,用于防止 XSS、数据注入等攻击,是现代 Web 安全的重要防线。
内容安全策略(Content Security Policy,简称 CSP)是一种声明式机制,让 Web 开发者可以精确控制页面能够加载哪些资源。通过指定允许的资源来源,CSP 可以有效防止 XSS 攻击、数据泄露等安全问题。
┌──────────────┐ ┌──────────────┐
│ 浏览器 │ ← HTTP 响应头 ── │ 服务器 │
└──────────────┘ └──────────────┘
│
↓ 解析 CSP 头
┌──────────────────────────────────────────────────┐
│ Content-Security-Policy: default-src 'self' │
│ ↓ │
│ ┌────────────────────────────────────────────┐ │
│ │ 资源加载策略 │ │
│ │ • 脚本:只允许同源 │ │
│ │ • 样式:只允许同源 │ │
│ │ • 图片:只允许同源 │ │
│ │ • 连接:只允许同源 │ │
│ └────────────────────────────────────────────┘ │
│
↓ 加载资源时检查
┌──────────────┐ ┌──────────────┐
│ 允许加载 │ 或 违规报告 ──→ │ 阻止加载 │
└──────────────┘ └──────────────┘| 功能 | 说明 | 防护目标 |
|---|---|---|
| 限制资源来源 | 只允许加载指定来源的资源 | XSS、数据泄露 |
| 禁止内联脚本 | 阻止 <script> 内联代码执行 | XSS |
| 禁止 eval | 阻止动态代码执行 | XSS、代码注入 |
| 限制表单提交 | 控制表单提交目标 | 数据泄露 |
| 限制嵌入 | 控制页面被嵌入行为 | 点击劫持 |
| 违规报告 | 自动报告策略违规 | 安全监控 |
| 指令 | 控制对象 | 示例 |
|---|---|---|
default-src | 各类资源的默认来源 | default-src 'self' |
script-src | 脚本 | script-src 'self' https://cdn.example.com |
style-src | 样式 | style-src 'self' 'unsafe-inline' |
img-src | 图片 | img-src 'self' data: |
connect-src | XHR/fetch/WebSocket 连接 | connect-src 'self' wss://ws.example.com |
font-src | 字体 | font-src 'self' https://fonts.gstatic.com |
frame-src | iframe 内容 | frame-src 'self' |
frame-ancestors | 本页面可被谁嵌入 | frame-ancestors 'self' |
object-src | <object>/<embed> 等插件 | object-src 'none' |
base-uri | <base> 标签目标 | base-uri 'self' |
form-action | 表单提交目标 | form-action 'self' |
upgrade-insecure-requests | 强制升级 HTTPS | upgrade-insecure-requests |
| 来源表达式 | 含义 | 示例 |
|---|---|---|
'self' | 同源资源 | 'self' |
| 域名 | 指定主机的资源 | https://cdn.example.com |
| 协议 | 特定协议所有资源 | https: data: blob: |
'none' | 禁止任何来源 | object-src 'none' |
'unsafe-inline' | 允许内联(不推荐) | style-src 'unsafe-inline' |
'unsafe-eval' | 允许 eval(不推荐) | script-src 'unsafe-eval' |
'nonce-xxx' | 匹配一次性令牌 | 'nonce-abc123' |
'sha256-xxx' | 匹配内容哈希 | 'sha256-jzgBGA4U…' |
Content-Security-Policy: script-src 'self' 'nonce-abc123randomtoken'<!-- 服务端生成的 nonce 必须匹配 -->
<script nonce="abc123randomtoken">
console.log('安全的内联脚本');
</script>Content-Security-Policy: script-src 'self' 'sha256-jzgBGA4UWFFmpOBq0JpdsySukE1FrEN5bUpoK8Z29fY='<!-- 脚本内容必须精确匹配 -->
<script>
console.log('这个内容会被 hash 验证');
</script>生成 Hash 的方法:
// 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# 最严格(推荐)
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';Content-Security-Policy: default-src 'self'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'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-requestsconst express = require('express');
const helmet = require('helmet');
const crypto = require('crypto');
const app = express();
// CSP 中间件(使用 nonce)
app.use((req, res, next) => {
const nonce = res.locals.nonce;
res.setHeader(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self'`
);
next();
});
// 路由:返回带 nonce 的页面
app.get('/', (req, res) => {
res.send(`
<html>
<head>
<title>CSP Nonce 示例</title>
</head>
<body>
<script nonce="${res.locals.nonce}">
console.log('安全的内联脚本');
</script>
</body>
</html>
`);
});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 "0" 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(高级配置)
# 需要 11-Nginx基础概述 + lua-11-Nginx基础概述-module# 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',
# ...
]<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">注意:Meta 标签方式有局限性:
frame-ancestors、report-uri 等指令<!-- ❌ 这些内联代码会被 CSP 阻止 -->
<script>
console.log('内联脚本');
</script>
<div onclick="alert('事件处理器')">点击</div>
<style>
.content { color: red; }
</style>// 服务端生成 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 模板 -->
<script nonce="<%= nonce %>">
console.log('安全的内联脚本');
</script>
<style nonce="<%= nonce %>">
.content { color: red; }
</style>// 预先计算 hash
const scriptHash = 'sha256-jzgBGA4UWFFmpOBq0JpdsySukE1FrEN5bUpoK8Z29fY=';
// 设置 CSP 头
res.setHeader(
'Content-Security-Policy',
`script-src 'self' '${scriptHash}'`
);Content-Security-Policy: script-src 'strict-dynamic' 'nonce-abc123'<script nonce="abc123">
// 动态创建的脚本会继承 nonce 的信任
const script = document.createElement('script');
script.src = 'https://cdn.example.com/script.js';
document.head.appendChild(script);
</script><!-- ❌ 不推荐:内联事件处理器 -->
<button onclick="handleClick()">点击</button>
<!-- ✅ 推荐:使用 JavaScript 绑定事件 -->
<button id="myButton">点击</button>
<script src="/js/app.js"></script>// /js/app.js
document.getElementById('myButton').addEventListener('click', handleClick);# 使用 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"}]}// 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();
});{
"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)"
}
}/**
* CSP 报告分析器
*/
class CspReportAnalyzer {
constructor() {
this.reports = [];
}
/**
* 添加报告
*/
addReport(report) {
this.reports.push(report);
}
/**
* 按违规指令聚合分析
*/
analyze() {
const byDirective = {};
for (const report of this.reports) {
const directive = report['effective-directive'] || report['violated-directive'];
byDirective[directive] = (byDirective[directive] || 0) + 1;
}
const recommendations = Object.entries(byDirective)
.sort((a, b) => b[1] - a[1])
.map(([directive, count]) =>
`${directive} 违规 ${count} 次,请检查对应资源的来源配置`
);
return recommendations;
}
}
const analyzer = new CspReportAnalyzer();阶段 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
├── 启用违规报告
├── 定期审计策略
└── 持续优化// 仅报告模式(不阻止违规)
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
reportUri: ['/csp-report']
},
reportOnly: true // 仅报告,不阻止
})
);// 电商网站 CSP 配置示例
const ecomCspConfig = {
directives: {
// 默认策略
defaultSrc: ["'self'"],
// 脚本:允许 Google Analytics、支付网关
scriptSrc: [
"'self'",
'https://www.googletagmanager.com',
'https://ssl.google-analytics.com',
'https://js.stripe.com'
],
// 样式
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
// 图片与连接
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: [
"'self'",
'https://www.google-analytics.com',
'https://api.stripe.com'
],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
// 支付 iframe
frameSrc: ["'self'", 'https://js.stripe.com', 'https://hooks.stripe.com'],
// 其他
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'self'"]
}
};
app.use(helmet.contentSecurityPolicy(ecomCspConfig));// React SPA CSP 配置
const spaCspConfig = {
directives: {
defaultSrc: ["'self'"],
// 脚本:CDN + nonce
scriptSrc: [
"'self'",
'https://cdn.example.com',
(req, res) => `'nonce-${res.locals.nonce}'`
],
// 样式:SPA 运行时注入样式通常需要 unsafe-inline 或 nonce
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'blob:'],
connectSrc: ["'self'", 'https://api.example.com', 'wss://ws.example.com'],
// 其他
objectSrc: ["'none'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"]
}
};┌─────────────────────────────────────────────────────────┐
│ XSS 防护层次 │
├─────────────────────────────────────────────────────────┤
│ 第1层:输入验证 │
│ ├─ 白名单验证 │
│ └─ 格式验证 │
├─────────────────────────────────────────────────────────┤
│ 第2层:输出编码 │
│ ├─ HTML 实体编码 │
│ └─ URL 编码 │
├─────────────────────────────────────────────────────────┤
│ 第3层:CSP(本节重点) │
│ ├─ 限制脚本来源 │
│ ├─ 禁止内联脚本 │
│ └─ 禁止 eval │
├─────────────────────────────────────────────────────────┤
│ 第4层:HttpOnly Cookie │
│ └─ 防止 JavaScript 读取敏感 Cookie │
└─────────────────────────────────────────────────────────┘CSP 作为 XSS 防护的最后一道防线:
# CSP frame-ancestors 替代 X-Frame-Options
Content-Security-Policy: frame-ancestors 'self' https://trusted.com
# 同时使用 X-Frame-Options(兼容性)
X-Frame-Options: SAMEORIGIN# CSP form-action 限制表单提交目标
Content-Security-Policy: form-action 'self'
# 配合 CSRF Token 使用
# form-action 限制提交目标,CSRF Token 验证请求来源// 综合安全配置
app.use(
// CSP
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"]
}
}),
helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}),
// CSRF 保护
csrf({ cookie: true })
);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.解决方案:
# 方案1:使用 nonce
script-src 'self' 'nonce-abc123';
# 方案2:使用 hash
script-src 'self' 'sha256-xxx';方案3:重构代码(推荐)——将内联脚本移到外部文件。
Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self'".解决方案:
<!-- ❌ 不推荐 -->
<button onclick="handleClick()">点击</button>
<!-- ✅ 推荐 -->
<button id="btn">点击</button>
<script>
document.getElementById('btn').addEventListener('click', handleClick);
</script>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'".解决方案:
// ❌ 不推荐:使用 eval
eval(code);
new Function(code);
setTimeout(code, 100);
// ✅ 推荐:重构代码
// 如果必须使用,添加 'unsafe-eval'(不推荐)
script-src 'self' 'unsafe-eval';Refused to load the script 'https://example.com/script.js' because it violates the following Content Security Policy directive: "script-src 'self'".解决方案:
# 添加第三方域名到白名单
script-src 'self' https://example.com;
# 或使用 strict-dynamic(推荐)
script-src 'strict-dynamic' 'nonce-abc123';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';回答:影响很小,主要是:
建议:使用 Report-Only 模式先行测试。
回答:将第三方域名添加到对应的 CSP 指令:
# 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;回答:不能完全防止,但能大幅降低风险:
| 场景 | CSP 是否有效 |
|---|---|
| 外部注入的恶意脚本 | ✅ 有效 |
| 内联脚本注入 | ✅ 有效(配置正确时) |
| DOM 型 XSS(基于用户输入) | ⚠️ 部分有效 |
| 服务端 XSS(返回恶意内容) | ⚠️ 部分有效 |
建议:CSP 应与其他防护措施配合使用。
回答:
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 }));回答:
| 特性 | Nonce | Hash |
|---|---|---|
| 动态内容 | ✅ 适合 | ❌ 不适合 |
| 静态内容 | ✅ 适合 | ✅ 适合 |
| 实现复杂度 | 中 | 低 |
| 性能 | 每次请求生成 | 预先计算 |
推荐:
strict-dynamic回答:使用 connect-src 指令:
# 允许 WebSocket 连接
connect-src 'self' wss://ws.example.com;
# 允许所有同源 WebSocket
connect-src 'self' ws: wss:;回答:正确配置的 CSP 对 SEO 没有负面影响:
💡 提示:CSP 是防御 XSS 的重要手段,但不是万能的。推荐使用渐进式部署策略,从 Report-Only 模式开始,逐步收紧策略。同时应配合其他安全措施(如输入验证、输出编码、HttpOnly Cookie 等)形成纵深防御体系。