CSP 内容安全策略实战指南:从 XSS 防御到生产部署

深入讲解 Content-Security-Policy 的 nonce、hash、strict-dynamic 策略,涵盖 Vue/React 框架集成、CSP 报告监控及常见绕过防御,帮助开发者构建真正的 XSS 防线。

安全与密码 2026-05-31 12 分钟

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'

这个策略的含义是:

  1. 带有 nonce-abc123<script> 标签可以执行
  2. 这个脚本通过 document.createElement('script') 动态创建的子脚本也自动被信任
  3. 子脚本再创建的脚本同样被信任——信任链无限传播
  4. 没有 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.jsheaders 配置:

// 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 应用。

📚 相关文章