2025 年 OWASP Top 10 报告显示,XSS(跨站脚本攻击)仍然稳居 Web 安全漏洞前三,而 CSP(Content-Security-Policy,内容安全策略)是目前浏览器原生提供的最强大 XSS 防御机制。但现实中,超过 70% 的网站要么没有配置 CSP,要么配置了 unsafe-inline 等于形同虚设。本文将从实际部署角度出发,带你掌握 CSP 的核心策略、框架集成方案和生产环境监控。
🔐 一、CSP 核心机制与策略选择
CSP 的本质是告诉浏览器:哪些来源的资源可以加载和执行。当页面中出现不在白名单内的脚本时,浏览器直接拒绝执行,从根源上阻断 XSS 攻击。
1.1 三代脚本策略对比
CSP 的脚本控制策略经历了三代演进,理解它们的区别是正确部署 CSP 的前提:
| 策略 | 机制 | XSS 防护力 | 兼容性 | 推荐场景 |
|---|---|---|---|---|
unsafe-inline |
允许所有内联脚本 | ❌ 几乎无效 | 全部浏览器 | 不推荐 |
| nonce-based | 只允许带正确 nonce 的内联脚本 | ✅ 强 | CSP Level 2+ | 静态站点、传统 SSR |
| hash-based | 只允许匹配 hash 的脚本块 | ✅ 强 | CSP Level 2+ | 脚本固定的场景 |
strict-dynamic |
nonce 信任传播:nonce 合法的脚本动态创建的子脚本也自动信任 | ✅ 最强 | CSP Level 3 | 现代 SPA、SSR 应用 |
⚠️ 警告:
unsafe-inline在有script-src的情况下几乎无法防御 XSS。如果你的 CSP 策略包含unsafe-inline,请立即升级到 nonce 或 hash 策略。
1.2 Nonce 策略实战
Nonce(Number used once,一次性随机数)是最常用的 CSP 策略。每次页面请求生成一个随机 nonce 值,只有携带该 nonce 的 <script> 标签才能执行。
Node.js + Express 实现:
// nonce-based CSP 中间件(Express)
import crypto from 'crypto';
function cspMiddleware(req, res, next) {
// 每次请求生成唯一的 nonce
const nonce = crypto.randomBytes(16).toString('base64');
// 将 nonce 注入到 res.locals,供模板引擎使用
res.locals.cspNonce = nonce;
// 设置 CSP 头
res.setHeader('Content-Security-Policy', [
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'nonce-${nonce}' 'unsafe-inline'`, // 样式允许 unsafe-inline(见下文说明)
`img-src 'self' data: https:`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`report-uri /csp-report`,
].join('; '));
next();
}
💡 **提示:**样式(
style-src)通常需要保留'unsafe-inline',因为很多 CSS-in-JS 框架和动画库依赖内联样式。可以用'unsafe-hashes'替代(CSP Level 3),但浏览器支持度稍低。
在模板中使用 nonce:
<!-- 在服务端渲染的 HTML 中使用 nonce -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- ✅ 正确:带 nonce 的内联脚本会被执行 -->
<script nonce="{{ cspNonce }}">
window.__INITIAL_STATE__ = {{ initialState }};
</script>
<!-- ✅ 正确:外部脚本配合 strict-dynamic -->
<script nonce="{{ cspNonce }}" src="/app.js"></script>
</head>
<body>
<!-- ❌ 错误:没有 nonce 的内联脚本会被 CSP 阻止 -->
<script>alert('this will be blocked')</script>
</body>
</html>
1.3 strict-dynamic 的信任传播
strict-dynamic 是 CSP Level 3 引入的关键特性。它解决了动态脚本加载的信任问题:
script-src 'nonce-abc123' 'strict-dynamic'
这个策略的含义是:
- 带有
nonce-abc123的<script>标签可以执行 - 这个脚本通过
document.createElement('script')动态创建的子脚本也自动被信任 - 子脚本再创建的脚本同样被信任——信任链无限传播
- 没有 nonce 的静态脚本(如
<script src="...">不带 nonce)会被阻止
⚡ 关键结论:
strict-dynamic配合 nonce 是现代 Web 应用的最优策略。它解决了传统 nonce 策略无法覆盖动态加载脚本(如第三方 SDK 懒加载)的问题,同时保持了强大的 XSS 防护。
🚀 二、主流框架的 CSP 集成方案
理论搞清楚了,但在 Vue、React、Next.js 等框架中落地 CSP 会遇到各种实际问题。以下是经过生产验证的方案。
2.1 Vue 3 + Nuxt 3 集成
Nuxt 3 使用 Nitro 作为服务端引擎,可以通过服务端中间件注入 CSP 头:
// server/middleware/csp.ts(Nuxt 3 服务端中间件)
import crypto from 'crypto'
export default defineEventHandler((event) => {
const nonce = crypto.randomBytes(16).toString('base64')
// 存储到 event.context,供后续使用
event.context.cspNonce = nonce
// 设置 CSP 响应头
setResponseHeader(event, 'Content-Security-Policy', [
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: blob: https:`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' https://api.example.com wss:`,
`worker-src 'self' blob:`,
`frame-ancestors 'none'`,
`upgrade-insecure-requests`,
].join('; '))
})
在 Nuxt 3 组件中使用 nonce:
<!-- pages/index.vue -->
<template>
<div>
<h1>安全的页面</h1>
<!-- Nuxt 3 的 NuxtScript 组件支持 nonce 属性 -->
</div>
</template>
<script setup lang="ts">
// 在服务端获取 nonce
const nonce = useRequestEvent()?.context?.cspNonce ?? ''
// 使用 useHead 注入带 nonce 的外部脚本
useHead({
script: [
{
src: 'https://analytics.example.com/tracker.js',
tagPosition: 'head',
// 注意:Nuxt 3 的 useHead 可能需要手动处理 nonce
// 更可靠的方式是通过 SSR 模板注入
}
]
})
</script>
⚠️ **警告:**Nuxt 3 的
useHead不直接支持nonce属性传递到<script>标签。推荐方案是在app.vue中通过ssrContext手动注入 nonce,或使用自定义 Nitro 插件在 HTML 响应中进行字符串替换。
2.2 React + Next.js 集成
Next.js 15+ 内置了 CSP 支持,可以通过 next.config.js 的 headers 配置:
// next.config.js
import crypto from 'crypto';
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
// 注意:Next.js 的 headers 配置是静态的,
// nonce 需要通过 middleware 动态注入
value: [
"script-src 'self' 'unsafe-eval'", // 开发模式需要 unsafe-eval
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
].join('; '),
},
],
},
];
},
};
export default nextConfig;
生产环境推荐用 Next.js Middleware 动态注入 nonce:
// middleware.ts(Next.js 15+)
import { NextResponse, NextRequest } from 'next/server';
import crypto from 'crypto';
export function middleware(request: NextRequest) {
const nonce = crypto.randomBytes(16).toString('base64');
const cspHeader = [
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' blob: data: https:`,
`font-src 'self'`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`upgrade-insecure-requests`,
].join('; ');
const response = NextResponse.next();
// 设置 CSP 头
response.headers.set('Content-Security-Policy', cspHeader);
// 将 nonce 通过自定义头传递给页面组件
response.headers.set('x-csp-nonce', nonce);
return response;
}
export const config = {
matcher: [
// 排除静态资源和 API 路由
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
2.3 Vue/React 中样式策略的特殊处理
CSS-in-JS 库(如 styled-components、Emotion、Vue 的 <style scoped>)会产生内联样式。对于 CSP 的 style-src 指令,有两种处理方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
style-src 'unsafe-inline' |
允许所有内联样式 | 兼容性好,零改造 | 理论上存在 CSS 注入风险 |
style-src 'unsafe-hashes' 'sha256-xxx' |
只允许匹配 hash 的样式属性 | 更安全 | 不适用于动态样式,浏览器支持不全 |
style-src 'self' + 外部样式文件 |
禁止内联样式,只允许外部 CSS | 最安全 | 需要重构所有内联样式 |
💡 **提示:**CSS 注入的实际危害远低于 JS 注入(无法直接执行代码、窃取数据)。在
style-src中保留'unsafe-inline'是业界普遍接受的妥协,优先把精力放在script-src的安全加固上。
📊 三、CSP 报告、监控与渐进式部署
直接在生产环境强制开启 CSP 可能导致功能异常。正确的做法是渐进式部署:先用报告模式收集数据,修复问题后再强制执行。
3.1 Report-Only 模式
CSP 提供了 Content-Security-Policy-Report-Only 头,它不阻止任何资源加载,只记录违规行为:
// 渐进式 CSP 部署:Phase 1 - Report-Only
function cspReportOnlyMiddleware(req, res, next) {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
// Report-Only 模式:不阻止,只报告
res.setHeader('Content-Security-Policy-Report-Only', [
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`connect-src 'self' https://api.example.com`,
// violation-endpoint 指定违规报告的接收地址
`report-to csp-endpoint`,
].join('; '));
// 配置 Reporting API(现代方案)
res.setHeader('Reporting-Endpoints', 'csp-endpoint="/csp-report"');
next();
}
3.2 接收和分析 CSP 违规报告
浏览器发送的 CSP 违规报告是 JSON 格式,包含详细的违规信息:
// CSP 违规报告接收端(Express)
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'] || req.body;
console.log('CSP 违规报告:', {
// 违反的指令
violatedDirective: report['violated-directive'],
// 实际加载的资源
blockedUri: report['blocked-uri'],
// 页面来源
documentUri: report['document-uri'],
// 原始策略
originalPolicy: report['original-policy'],
// 脚本样本(CSP Level 3,用于调试)
scriptSample: report['script-sample'],
// 行号和列号(有助于定位问题)
lineNumber: report['line-number'],
columnNumber: report['column-number'],
// 时间戳
timestamp: new Date().toISOString(),
});
// 存储到数据库(这里简化为日志输出)
// 生产环境建议存储到 ELK、ClickHouse 等分析系统
res.status(204).end();
});
违规报告示例:
{
"csp-report": {
"document-uri": "https://example.com/dashboard",
"referrer": "https://example.com/login",
"blocked-uri": "https://evil.com/malicious.js",
"violated-directive": "script-src-elem",
"original-policy": "script-src 'nonce-abc123' 'strict-dynamic'; report-to csp-endpoint",
"disposition": "report",
"status-code": 200,
"script-sample": "alert(document.cookie)"
}
}
3.3 渐进式部署三步走
📌 **记住:**永远不要一步到位开启 CSP 强制模式。按照以下三步走,用 2-4 周时间完成部署。
第一步:报告收集(1-2 周)
部署 Content-Security-Policy-Report-Only 头,策略设置为你期望的最终策略。收集所有违规报告,分析哪些是正常业务脚本被误报、哪些是真正的安全风险。
第二步:修复误报(1 周)
根据报告修复问题:
- ❌ 未带 nonce 的第三方脚本 → ✅ 添加 nonce 或改为外部加载
- ❌ 内联事件处理器(
onclick="...")→ ✅ 改用addEventListener - ❌ 动态创建的
<script>标签 → ✅ 确保有strict-dynamic策略
第三步:强制执行(持续监控)
将 Content-Security-Policy-Report-Only 替换为 Content-Security-Policy,同时保留报告端点。此后浏览器会真正阻止违规资源并发送报告。
// Phase 3:强制执行 + 持续监控
res.setHeader('Content-Security-Policy', [...].join('; '));
res.setHeader('Reporting-Endpoints', 'csp-endpoint="/csp-report"');
3.4 常见 CSP 绕过手段与防御
了解攻击者的绕过手段,才能写出更健壮的 CSP 策略:
| 绕过手段 | 原理 | 防御措施 |
|---|---|---|
| JSONP 回调注入 | 利用允许的域名上的 JSONP 接口执行任意代码 | 不在 script-src 中放通配域名 |
| base URI 篡改 | 修改 <base> 标签改变相对路径解析 |
添加 base-uri 'self' 或 'none' |
| 开放重定向 | 通过允许的域名跳转到恶意页面 | 添加 navigate-to 限制(实验性) |
eval() 利用 |
通过 'unsafe-eval' 执行动态代码 |
绝不添加 'unsafe-eval' |
| SVG 注入 | SVG 文件中嵌入 <script> |
添加 img-src 限制或配置 CSP 对 SVG 的处理 |
// ❌ 危险配置:容易被绕过
const dangerousCSP = [
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // 等于没有 CSP
"script-src *.googleapis.com", // 通配域名 → JSONP 绕过
"script-src 'self' https:", // 任何 HTTPS 源 → 等于没有限制
].join('; ');
// ✅ 安全配置:最小权限原则
const safeCSP = [
"script-src 'self' 'nonce-{nonce}' 'strict-dynamic'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https://cdn.example.com",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests",
].join('; ');
⚡ 关键结论:CSP 的安全性取决于策略中最薄弱的环节。一个
'unsafe-inline'就能让整个script-src形同虚设。配置 CSP 时遵循最小权限原则:只允许业务必需的来源,其他一律禁止。
💡 四、CSP 与 Subresource Integrity(SRI)
SRI(子资源完整性)是 CSP 的黄金搭档。它允许你对加载的外部资源(JS/CSS)进行 hash 校验,防止 CDN 被篡改后加载恶意代码。
<!-- SRI + CSP 联合使用 -->
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-abc123..."
crossorigin="anonymous"
nonce="{{ cspNonce }}">
</script>
// 自动为构建产物生成 SRI hash 的 Node.js 脚本
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
function generateSRI(filePath) {
const content = readFileSync(filePath, 'utf-8');
const hash = createHash('sha384').update(content).digest('base64');
return `sha384-${hash}`;
}
// 生成 HTML 中的 integrity 属性
const integrity = generateSRI('./dist/lib.js');
console.log(`integrity="${integrity}"`);
// 输出: integrity="sha384-x7A+BC9..."
💡 **提示:**SRI 只对外部资源有效(
src="..."的<script>和<link>),对内联脚本/样式无效。配合 CSP 的 nonce 策略使用,可以实现「内联脚本靠 nonce 控制、外部脚本靠 SRI 校验」的双重防护。
🔧 五、生产环境 CSP 调试技巧
部署 CSP 过程中最痛苦的是调试被阻止的资源。以下是实用的调试方法:
5.1 浏览器 DevTools 调试
Chrome DevTools 的 Application → Security 面板会显示当前页面的 CSP 策略状态。Console 面板中被 CSP 阻止的资源会有明确的错误提示:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'nonce-abc123' 'strict-dynamic'"
5.2 CSP Evaluator 工具
Google 提供的 CSP Evaluator 可以自动分析你的 CSP 策略是否存在已知绕过风险。将你的 CSP 策略粘贴进去,它会标记所有潜在问题。
5.3 自动化 CSP 测试
在 CI/CD 中集成 CSP 测试,确保新代码不会引入 CSP 违规:
// CSP 合规性测试(Playwright 示例)
import { test, expect } from '@playwright/test';
test('页面不应有 CSP 违规', async ({ page }) => {
const violations = [];
// 监听 CSP 违规事件
page.on('console', msg => {
if (msg.text().includes('Content Security Policy')) {
violations.push(msg.text());
}
});
// 监控 report-to 接口
await page.route('**/csp-report', async route => {
const postData = route.request().postDataJSON();
violations.push(postData);
await route.fulfill({ status: 204 });
});
await page.goto('https://example.com/dashboard');
await page.waitForLoadState('networkidle');
// 断言:不应有 CSP 违规
expect(violations).toHaveLength(0);
});
✅ 总结与建议
CSP 不是一个「设置了就安全」的银弹,它需要持续的维护和监控。以下是落地建议:
立即行动项:
- ✅ 检查你的网站是否已配置 CSP 头(打开 DevTools → Network → 查看响应头)
- ✅ 如果只有
X-XSS-Protection,它已被现代浏览器废弃,需要迁移到 CSP - ✅ 从
Content-Security-Policy-Report-Only开始,不要直接强制执行
策略选择建议:
- ✅
script-src使用'nonce-{random}' 'strict-dynamic'组合 - ✅
style-src暂时保留'unsafe-inline'(CSS 注入风险远低于 JS) - ❌ 永远不要在
script-src中使用'unsafe-inline'或'unsafe-eval' - ❌ 永远不要在
script-src中使用通配符域名(*.example.com)
推荐工具:
- 🔧 CSP Evaluator — Google 出品的 CSP 策略分析工具
- 🔧 Report URI — CSP 违规报告收集和分析平台
- 🔧 Helmet.js — Node.js/Express 的安全头中间件,内置 CSP 支持
- 🔧 Nuxt Security — Nuxt 3 的安全模块,自动配置 CSP
CSP 的部署是一个渐进过程,不要追求一步到位。从 Report-Only 模式开始,逐步收紧策略,最终实现 script-src 'nonce' 'strict-dynamic' 的最强防护。你的用户值得一个真正安全的 Web 应用。