文章

CSP内容安全策略深度解析:从指令配置到nonce方案的防御实战

CSP 内容安全策略是 XSS 纵深防御的核心:讲清指令配置(default-src、script-src)、nonce/hash 方案与上报机制。 掌握后能说清「为什么 CSP 能拦住内联脚本注入」。

CSP内容安全策略深度解析:从指令配置到nonce方案的防御实战

一句话概括

CSP(Content Security Policy)是通过 HTTP 响应头精确控制浏览器允许加载哪些资源的白名单机制,核心价值是为 XSS 攻击提供「即使脚本注入成功也无法执行」的最后一道防护墙。

核心知识点

1. CSP 基本结构

1
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src * data:; frame-ancestors 'none'

每个指令由「资源类型-src」组成,default-src 是对所有未指定类型的兜底策略。值可以是 'self'(同源)、URL、'none'、'unsafe-inline'、'unsafe-eval'、'nonce-xxx'、'sha256-xxx' 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在 Express 中设置 CSP
app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', [
    "default-src 'self'",
    "script-src 'self' 'nonce-" + req.nonce + "'",
    "style-src 'self' 'unsafe-inline'",
    "img-src * data: blob:",
    "font-src 'self' https://fonts.gstatic.com",
    "frame-ancestors 'none'",
    "connect-src 'self' https://api.myapp.com",
  ].join('; '));
  next();
});

2. nonce 方案:安全地允许内联脚本

'unsafe-inline' 让 CSP 对内联脚本的防护形同虚设。nonce 方案为每个请求生成一次性随机值,只有在 HTML 中带有匹配 nonce 的 <script> 才会执行。

1
2
3
4
5
6
7
8
<!-- 服务端每次渲染生成一个新 nonce -->
<script nonce="abc123random">
  // ✅ CSP 头中有 script-src 'nonce-abc123random',这个脚本可以执行
  console.log('safe inline script');
</script>

<!-- ❌ 攻击者注入的脚本没有匹配的 nonce,被浏览器拦截 -->
<script>alert('xss')</script>
1
2
3
4
5
6
7
8
9
10
11
// Express 中间件生成 nonce
const crypto = require('crypto');

app.use((req, res, next) => {
  req.nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = req.nonce;
  next();
});

// 在模板中使用 nonce(以 EJS 为例)
// <script nonce="<%= nonce %>">...</script>

为什么 nonce 比 hash 更实用? nonce 对所有内联脚本通用,而 hash 需要精确匹配每个脚本内容(内容一变 hash 就变),维护成本高。生产环境推荐 nonce。

3. 核心指令速查

指令控制范围面试高频场景
script-srcJavaScript 执行源最关键的指令,XSS 核心防御
style-srcCSS 来源防止 CSS 注入窃取数据
img-src图片来源防止 img 标签用于 CSRF
connect-srcfetch/XHR/WebSocket限制 API 请求目标
frame-ancestors谁可以 iframe 嵌入替代 X-Frame-Options
form-actionform 提交目标防止表单劫持
base-uri<base> 标签防止 base 标签劫持相对路径
report-uri / report-to违规上报地址监控 CSP 违规情况

4. 报告模式:先监控再强制执行

1
2
# 只报告不拦截(用于收集数据、评估影响)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
1
2
3
4
5
6
7
8
9
10
11
// 接收 CSP 报告的服务端
app.post('/csp-report', (req, res) => {
  const report = req.body['csp-report'];
  console.log('CSP Violation:', {
    blocked: report['blocked-uri'],
    violated: report['violated-directive'],
    page: report['document-uri'],
    script: report['script-sample']
  });
  res.sendStatus(204);
});

渐进式部署策略:先用 Report-Only 模式收集一周数据,修正误报的合法脚本(如第三方 SDK),确认无误后切换到强制执行模式。

5. CSP 与框架集成

1
2
3
4
5
6
7
8
9
// Vite/Webpack 构建时自动生成 nonce 并注入
// __webpack_nonce__ = 'abc123'

// React:天然安全(JSX 自动转义),但仍需 CSP 防第三方脚本
// 关键是防止 eval 和 inline
// CSP: script-src 'self' 'wasm-unsafe-eval'
// 如果用了 styled-components 需要 style-src 'unsafe-inline'

// 如果你用 Create React App,INLINE_RUNTIME_CHUNK=false 可以避免内联脚本

其实你每天都在用

  • 微信网页支付的 JSAPI:微信支付 SDK 注入的脚本需要 CSP 显式放行 https://res.wx.qq.com,否则支付功能直接挂
  • Google Analytics / 百度统计:所有第三方统计脚本都需要在 script-src 中显式列出,这是 CSP 最常见的「维护成本」
  • 富文本编辑器:Quill、TinyMCE 等编辑器会动态创建 style 标签,需要 style-src 'unsafe-inline' 或用 nonce 方案,这是 CSP 最常见的技术挑战
  • Chrome 扩展的 content_security_policy:每个扩展的 manifest.json 里都有 CSP 声明,限制扩展可以加载什么
  • Gmail 的邮件图片:Gmail 默认不加载邮件中的外部图片,本质就是一个 img-src 策略 —— 只不过它在应用层而非 HTTP 头实现

常见误解(FAQ)

❌ 误区:CSP 可以替代 XSS 防御

CSP 是 XSS 的「最后防线」而非「替代」。如果攻击者能注入脚本,说明输入过滤或输出编码已经失败了。CSP 的理想状态应该是「即使有 XSS 漏洞,攻击者也无法执行脚本」,但它不能替代输入校验。

❌ 误区:default-src 'self' 就够了

太天真。default-src 'self' 只允许同源资源,但你的网站可能加载了 CDN 的库、字体的 CSS、第三方 API。更核心的问题是:如果攻击者能在你的同源下写入一个恶意 JS 文件(如图片上传漏洞写入 .js 到 uploads 目录),'self' 无法防御。

❌ 误区:script-src 'unsafe-inline' 'unsafe-eval' 等于没有 CSP

差不多。'unsafe-inline' 让所有内联脚本合法,'unsafe-eval' 让 eval() 合法 —— 这正是 XSS 攻击最常用的两个入口。如果业务必须用内联脚本,用 nonce 方案而不是 'unsafe-inline'。

❌ 误区:CSP 设置一次就不用管了

每引入一个新的第三方 SDK、CDN 域名、内联脚本,CSP 都需要更新。CSP 是「活的策略」,需要持续维护。

一句话总结

CSP 用 HTTP 头给浏览器下了「白名单」,nonce 是平衡安全性和内联脚本灵活性的最佳方案;先 Report-Only 再强制,先 'self' 再逐域放行,是最安全的部署节奏。

本文由作者按照 CC BY 4.0 进行授权