Appearance
内容安全策略 (CSP)
内容安全策略(Content Security Policy,CSP)是一种安全机制,通过声明式策略来控制浏览器可以加载和执行哪些资源,从而有效防范跨站脚本(XSS)、点击劫持等注入攻击。
CSP 基础概念
CSP 的作用
CSP 允许网站管理员定义:
- 哪些源可以加载脚本
- 哪些源可以加载样式
- 哪些源可以加载图片
- 哪些源可以发起网络请求
- 等等
CSP 的工作原理
CSP 通过 HTTP 响应头或 HTML meta 标签发送给浏览器,浏览器根据策略决定是否加载和执行资源。
CSP 指令详解
基础指令
javascript
// CSP 指令映射
const cspDirectives = {
// 默认策略指令
defaultSrc: {
description: '默认策略,当其他策略未指定时使用',
example: ["'self'"]
},
// 脚本相关指令
scriptSrc: {
description: '定义 JavaScript 脚本的允许来源',
example: ["'self'", "'unsafe-inline'", "'unsafe-eval'"]
},
// 样式相关指令
styleSrc: {
description: '定义 CSS 样式的允许来源',
example: ["'self'", "'unsafe-inline'"]
},
// 图片相关指令
imgSrc: {
description: '定义图片的允许来源',
example: ["'self'", "data:", "https:"]
},
// 字体相关指令
fontSrc: {
description: '定义字体文件的允许来源',
example: ["'self'", "https:", "data:"]
},
// 网络请求指令
connectSrc: {
description: '定义 XMLHttpRequest、WebSocket 等连接的允许来源',
example: ["'self'", "https://api.example.com"]
},
// 嵌入内容指令
frameSrc: {
description: '定义 frame 和 iframe 的允许来源',
example: ["'self'", "https://trusted-embed.com"]
},
// 插件指令
objectSrc: {
description: '定义插件(如 Flash)的允许来源',
example: ["'none'"] // 通常禁用
}
};
高级指令
javascript
// 高级 CSP 指令
const advancedCspDirectives = {
// 工作线程指令
workerSrc: {
description: '定义 Worker、SharedWorker、ServiceWorker 的允许来源',
example: ["'self'"]
},
// 嵌入子资源指令
childSrc: {
description: '定义嵌入子资源(如 Worker、frame)的允许来源',
example: ["'self'"]
},
// 框架祖先指令
frameAncestors: {
description: '定义哪些页面可以嵌入当前页面',
example: ["'none'"] // 防止点击劫持
},
// 表单动作指令
formAction: {
description: '定义表单提交的允许目标',
example: ["'self'"]
},
// 基础 URI 指令
baseUri: {
description: '定义 base 标签的允许来源',
example: ["'self'"]
},
// 导航指令
navigateTo: {
description: '定义导航目标的允许来源',
example: ["'self'"]
},
// 升级不安全请求指令
upgradeInsecureRequests: {
description: '自动将 HTTP 请求升级为 HTTPS',
example: [] // 无值
},
// 报告 URI 指令
reportUri: {
description: '定义 CSP 违规报告的接收地址',
example: ["/csp-report"]
},
// 媒体资源指令
mediaSrc: {
description: '定义音频、视频等媒体资源的允许来源',
example: ["'self'"]
}
};
CSP 源值详解
源值类型
javascript
// CSP 源值示例
const cspSourceValues = {
// 同源策略
self: {
value: "'self'",
description: '允许同源资源',
example: 'https://example.com 可以加载 https://example.com/path'
},
// 数据 URL
data: {
value: 'data:',
description: '允许 data: URL',
example: '允许内联图片 data:image/png;base64,...'
},
// 空白源
none: {
value: "'none'",
description: '不允许任何资源',
example: 'object-src \'none\' 禁止所有插件'
},
// 内联资源
unsafeInline: {
value: "'unsafe-inline'",
description: '允许内联脚本和样式',
warning: '降低安全性,谨慎使用'
},
// 动态执行
unsafeEval: {
value: "'unsafe-eval'",
description: '允许 eval 和动态代码执行',
warning: '高风险,尽量避免使用'
},
// 非安全协议
unsafeAllowRedirects: {
value: "'unsafe-redirects'",
description: '允许不安全的重定向'
},
// 哈希值
hash: {
value: "'sha256-abc123...'",
description: '允许特定哈希值的内联脚本',
example: 'script-src \'sha256-hashed-value\''
},
// 随机数
nonce: {
value: "'nonce-abc123'",
description: '允许特定随机数的内联脚本',
example: 'script-src \'nonce-unique-value\''
}
};
// 源值验证函数
function validateCspSource(source) {
const patterns = {
self: /^'self'$/,
none: /^'none'$/,
unsafeInline: /^'unsafe-inline'$/,
unsafeEval: /^'unsafe-eval'$/,
data: /^data:$/,
https: /^https:$/,
http: /^http:$/,
hash: /^'sha\d{1,3}-[a-zA-Z0-9+/=]{1,}$/,
nonce: /^'nonce-[a-zA-Z0-9+/=]{1,}'$/
};
for (const [type, pattern] of Object.entries(patterns)) {
if (pattern.test(source)) {
return { valid: true, type };
}
}
return { valid: false, type: 'unknown' };
}
CSP 实现示例
基础 CSP 配置
javascript
// 基础 CSP 策略
const basicCsp = {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // 开发阶段允许内联样式
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https:", "data:"],
connectSrc: ["'self'"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
baseUri: ["'self'"]
},
build() {
return Object.entries(this.directives)
.filter(([directive, sources]) => sources.length > 0)
.map(([directive, sources]) => {
const directiveName = directive
.replace(/([A-Z])/g, '-$1')
.toLowerCase();
const sourceList = sources.join(' ');
return `${directiveName} ${sourceList}`;
})
.join('; ');
}
};
// 在 Express.js 中使用
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', basicCsp.build());
next();
});
严格 CSP 配置
javascript
// 严格 CSP 策略(推荐用于生产环境)
const strictCsp = {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
// 使用 nonce 策略而不是 'unsafe-inline'
],
styleSrc: [
"'self'",
// 使用 nonce 策略而不是 'unsafe-inline'
],
imgSrc: ["'self'", "data:", "blob:", "https:"],
fontSrc: ["'self'", "https:", "data:"],
connectSrc: ["'self'", "https://api.example.com"],
mediaSrc: ["'self'"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
baseUri: ["'self'"],
manifestSrc: ["'self'"],
workerSrc: ["'self'"]
},
// 生成带 nonce 的策略
withNonce(nonce) {
const strictPolicy = { ...this.directives };
strictPolicy.scriptSrc = [...strictPolicy.scriptSrc, `'nonce-${nonce}'`];
strictPolicy.styleSrc = [...strictPolicy.styleSrc, `'nonce-${nonce}'`];
return Object.entries(strictPolicy)
.filter(([directive, sources]) => sources.length > 0)
.map(([directive, sources]) => {
const directiveName = directive
.replace(/([A-Z])/g, '-$1')
.toLowerCase();
const sourceList = sources.join(' ');
return `${directiveName} ${sourceList}`;
})
.join('; ');
},
// 生成带哈希的策略
withHashes(hashes) {
const strictPolicy = { ...this.directives };
strictPolicy.scriptSrc = [
...strictPolicy.scriptSrc,
...hashes.scripts.map(hash => `'sha256-${hash}'`)
];
strictPolicy.styleSrc = [
...strictPolicy.styleSrc,
...hashes.styles.map(hash => `'sha256-${hash}'`)
];
return Object.entries(strictPolicy)
.filter(([directive, sources]) => sources.length > 0)
.map(([directive, sources]) => {
const directiveName = directive
.replace(/([A-Z])/g, '-$1')
.toLowerCase();
const sourceList = sources.join(' ');
return `${directiveName} ${sourceList}`;
})
.join('; ');
}
};
// 带 nonce 的 CSP 中间件
function cspWithNonce(req, res, next) {
// 生成随机 nonce
const nonce = require('crypto').randomBytes(16).toString('hex');
// 将 nonce 添加到响应 locals,以便模板引擎使用
res.locals.nonce = nonce;
// 设置 CSP 头部
const cspPolicy = strictCsp.withNonce(nonce);
res.setHeader('Content-Security-Policy', cspPolicy);
next();
}
app.use(cspWithNonce);
CSP 报告机制
CSP 违规报告
javascript
// CSP 违规报告处理
app.use(express.json({ type: 'application/csp-report' }));
app.post('/csp-report', (req, res) => {
const report = req.body['csp-report'];
if (report) {
console.error('CSP Violation Report:', {
documentUri: report.document_uri,
violatedDirective: report.violated_directive,
originalPolicy: report.original_policy,
blockedUri: report.blocked_uri,
sourceFile: report.source_file,
lineNumber: report.line_number,
columnNumber: report.column_number,
effectiveDirective: report.effective_directive,
statusCode: report.status_code,
scriptSample: report.script_sample,
timestamp: new Date().toISOString()
});
// 可以将报告存储到数据库或发送警报
storeCspViolation(report);
}
res.status(204).end();
});
// 存储 CSP 违规的函数
async function storeCspViolation(report) {
try {
// 这里可以连接到数据库或日志服务
console.log('Storing CSP violation:', {
blockedUri: report.blocked_uri,
violatedDirective: report.violated_directive,
documentUri: report.document_uri,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Failed to store CSP violation:', error);
}
}
报告 URI 配置
javascript
// 带报告的 CSP 配置
const cspWithReporting = {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
reportUri: ["/csp-report"],
// 或使用现代的 report-to 指令
reportTo: ['csp-endpoint']
},
endpoints: {
'csp-endpoint': {
url: '/csp-report',
includeCspReportOnly: true
}
},
build() {
const directives = [];
for (const [directive, sources] of Object.entries(this.directives)) {
if (sources.length > 0) {
const directiveName = directive
.replace(/([A-Z])/g, '-$1')
.toLowerCase();
const sourceList = sources.join(' ');
directives.push(`${directiveName} ${sourceList}`);
}
}
return directives.join('; ');
}
};
// 设置报告端点
app.use((req, res, next) => {
res.setHeader('Reporting-Endpoints',
'csp-endpoint="/csp-report", default="/csp-report"'
);
res.setHeader('Content-Security-Policy', cspWithReporting.build());
next();
});
框架集成
Express.js 中的 CSP
javascript
const helmet = require('helmet');
// 使用 Helmet 配置 CSP
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https:", "data:"],
connectSrc: ["'self'", "https://api.example.com"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
baseUri: ["'self'"]
},
reportOnly: false // 是否只报告不阻止
}));
// 自定义 CSP 中间件
function customCspMiddleware(policy) {
return (req, res, next) => {
res.setHeader('Content-Security-Policy', policy);
next();
};
}
// 动态 CSP 策略
function createDynamicCsp(userRole = 'default') {
const policies = {
default: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"]
},
admin: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-eval'"], // 管理员可能需要动态执行
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:", "http:"],
connectSrc: ["'self'", "https://api.example.com", "https://admin-api.example.com"]
}
};
const userPolicy = policies[userRole] || policies.default;
return Object.entries(userPolicy)
.map(([directive, sources]) => {
const directiveName = directive
.replace(/([A-Z])/g, '-$1')
.toLowerCase();
const sourceList = sources.join(' ');
return `${directiveName} ${sourceList}`;
})
.join('; ');
}
// 根据用户角色设置 CSP
app.use((req, res, next) => {
const userRole = req.user?.role || 'default';
const cspPolicy = createDynamicCsp(userRole);
res.setHeader('Content-Security-Policy', cspPolicy);
next();
});
React 应用中的 CSP 支持
javascript
// React 应用的 CSP 兼容性处理
// 1. 避免内联事件处理器
// ❌ 错误:onClick="handleClick()"
// ✅ 正确:<button onClick={handleClick}>
// 2. 使用 React 的 dangerouslySetInnerHTML 时要小心
function SafeHtmlRenderer({ htmlContent, allowedDomains }) {
// 在服务端预处理 HTML 内容
const sanitizedHtml = sanitizeHtml(htmlContent, {
allowedTags: ['p', 'br', 'strong', 'em', 'ul', 'li', 'h1', 'h2', 'h3'],
allowedAttributes: {
a: ['href', 'title'],
img: ['src', 'alt']
},
allowedSchemes: ['https', 'http', 'data'],
allowedSchemesByTag: {
img: ['https', 'http', 'data']
}
});
return <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />;
}
// 3. 动态脚本加载的 CSP 友好方式
async function loadScript(src) {
return new Promise((resolve, reject) => {
// 如果有 nonce,使用它
const nonce = document.querySelector('meta[name="csp-nonce"]')?.content;
const script = document.createElement('script');
if (nonce) {
script.nonce = nonce;
}
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
CSP 调试和测试
CSP 违规检测
javascript
// CSP 违规检测工具
class CspViolationDetector {
constructor() {
this.violations = [];
this.init();
}
init() {
// 监听 CSP 违规事件
if (window.addEventListener) {
window.addEventListener('securitypolicyviolation', (e) => {
this.handleViolation(e);
});
}
}
handleViolation(event) {
const violation = {
blockedUri: event.blockedURI,
directive: event.violatedDirective,
effectiveDirective: event.effectiveDirective,
originalPolicy: event.originalPolicy,
sourceFile: event.sourceFile,
lineNumber: event.lineNumber,
columnNumber: event.columnNumber,
timestamp: new Date().toISOString()
};
this.violations.push(violation);
console.warn('CSP Violation:', violation);
// 可以发送到分析服务
this.reportViolation(violation);
}
reportViolation(violation) {
// 发送违规报告到服务器
if (navigator.sendBeacon) {
navigator.sendBeacon('/csp-report', JSON.stringify({
'csp-report': violation
}));
} else {
// 降级处理
fetch('/csp-report', {
method: 'POST',
body: JSON.stringify({ 'csp-report': violation }),
headers: { 'Content-Type': 'application/json' }
}).catch(err => console.error('Failed to report CSP violation:', err));
}
}
getViolations() {
return this.violations;
}
clearViolations() {
this.violations = [];
}
}
// 初始化检测器
if (typeof window !== 'undefined') {
window.cspDetector = new CspViolationDetector();
}
CSP 策略生成工具
javascript
// CSP 策略生成器
class CspPolicyBuilder {
constructor() {
this.directives = {};
}
addDirective(name, sources = []) {
const normalizedSources = Array.isArray(sources) ? sources : [sources];
const directiveName = name.replace(/([A-Z])/g, '-$1').toLowerCase();
if (!this.directives[directiveName]) {
this.directives[directiveName] = [];
}
this.directives[directiveName] = [
...this.directives[directiveName],
...normalizedSources
];
return this;
}
setDefaultSrc(sources) {
return this.addDirective('defaultSrc', sources);
}
setScriptSrc(sources) {
return this.addDirective('scriptSrc', sources);
}
setStyleSrc(sources) {
return this.addDirective('styleSrc', sources);
}
setImgSrc(sources) {
return this.addDirective('imgSrc', sources);
}
setConnectSrc(sources) {
return this.addDirective('connectSrc', sources);
}
setReportUri(uri) {
return this.addDirective('reportUri', uri);
}
build() {
return Object.entries(this.directives)
.filter(([_, sources]) => sources.length > 0)
.map(([directive, sources]) => {
const uniqueSources = [...new Set(sources)];
return `${directive} ${uniqueSources.join(' ')}`;
})
.join('; ');
}
// 生成报告模式的策略(只报告不阻止)
buildReportOnly() {
const policy = this.build();
return {
policy,
header: 'Content-Security-Policy-Report-Only'
};
}
}
// 使用示例
const cspBuilder = new CspPolicyBuilder();
const cspPolicy = cspBuilder
.setDefaultSrc(["'self'"])
.setScriptSrc(["'self'", "'unsafe-inline'"]) // 开发阶段
.setStyleSrc(["'self'", "'unsafe-inline'"]) // 开发阶段
.setImgSrc(["'self'", "data:", "https:"])
.setConnectSrc(["'self'", "https://api.example.com"])
.setReportUri(["/csp-report"])
.build();
console.log('Generated CSP:', cspPolicy);
CSP 最佳实践
1. 渐进式实施
javascript
// 分阶段 CSP 实施策略
const cspImplementation = {
phase1: {
name: 'Monitor Only',
policy: new CspPolicyBuilder()
.setDefaultSrc(["'self'"])
.setReportUri(["/csp-report"])
.build(),
header: 'Content-Security-Policy-Report-Only'
},
phase2: {
name: 'Basic Protection',
policy: new CspPolicyBuilder()
.setDefaultSrc(["'self'"])
.setScriptSrc(["'self'", "'unsafe-inline'"]) // 临时允许
.setStyleSrc(["'self'", "'unsafe-inline'"]) // 临时允许
.setImgSrc(["'self'", "data:", "https:"])
.setReportUri(["/csp-report"])
.build(),
header: 'Content-Security-Policy-Report-Only'
},
phase3: {
name: 'Strict Policy',
policy: new CspPolicyBuilder()
.setDefaultSrc(["'self'"])
.setScriptSrc(["'self'"]) // 移除 'unsafe-inline'
.setStyleSrc(["'self'"]) // 移除 'unsafe-inline'
.setImgSrc(["'self'", "data:", "https:"])
.setConnectSrc(["'self'", "https://api.example.com"])
.setFrameAncestors(["'none'"])
.setObjectSrc(["'none'"])
.setFormAction(["'self'"])
.setBaseUri(["'self'"])
.build(),
header: 'Content-Security-Policy'
}
};
// 根据实施阶段设置 CSP
function setCspByPhase(req, res, next) {
const phase = process.env.CSP_PHASE || 'phase1';
const config = cspImplementation[phase];
if (config) {
res.setHeader(config.header, config.policy);
}
next();
}
app.use(setCspByPhase);
2. 环境特定配置
javascript
// 环境特定 CSP 配置
function getCspConfig(environment = process.env.NODE_ENV) {
const configs = {
development: {
// 开发环境:允许更多灵活性
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:", "http:"],
fontSrc: ["'self'", "https:", "http:", "data:"],
connectSrc: ["'self'", "ws:", "http:", "https:"],
frameSrc: ["'self'", "https:"],
reportUri: ["/csp-report"]
},
reportOnly: true
},
staging: {
// 预发布环境:接近生产环境的策略
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https:", "data:"],
connectSrc: ["'self'", "https://api.staging.example.com"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
baseUri: ["'self'"],
reportUri: ["/csp-report"]
},
reportOnly: true
},
production: {
// 生产环境:最严格的策略
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https:", "data:"],
connectSrc: ["'self'", "https://api.example.com"],
mediaSrc: ["'self'"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
baseUri: ["'self'"],
upgradeInsecureRequests: true
},
reportOnly: false
}
};
return configs[environment] || configs.production;
}
// 应用环境特定 CSP
app.use((req, res, next) => {
const config = getCspConfig();
const policy = Object.entries(config.directives)
.filter(([_, sources]) => sources.length > 0)
.map(([directive, sources]) => {
const directiveName = directive
.replace(/([A-Z])/g, '-$1')
.toLowerCase();
const sourceList = sources.join(' ');
return `${directiveName} ${sourceList}`;
})
.join('; ');
const headerName = config.reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
res.setHeader(headerName, policy);
next();
});
总结
CSP 是防范 XSS 等注入攻击的有效工具:
- 理解指令 - 熟悉各种 CSP 指令的作用
- 合理配置 - 根据应用需求配置适当的策略
- 使用 nonce/hash - 避免使用 'unsafe-inline'
- 实施监控 - 设置报告机制监控违规
- 分阶段实施 - 从宽松到严格逐步推进
- 环境区分 - 不同环境使用不同策略
正确实施 CSP 可以显著提高 Web 应用的安全性。