CSP内容安全策略深度解析:从指令配置到nonce方案的防御实战
CSP 内容安全策略是 XSS 纵深防御的核心:讲清指令配置(default-src、script-src)、nonce/hash 方案与上报机制。 掌握后能说清「为什么 CSP 能拦住内联脚本注入」。
一句话概括
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-src | JavaScript 执行源 | 最关键的指令,XSS 核心防御 |
style-src | CSS 来源 | 防止 CSS 注入窃取数据 |
img-src | 图片来源 | 防止 img 标签用于 CSRF |
connect-src | fetch/XHR/WebSocket | 限制 API 请求目标 |
frame-ancestors | 谁可以 iframe 嵌入 | 替代 X-Frame-Options |
form-action | form 提交目标 | 防止表单劫持 |
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' 再逐域放行,是最安全的部署节奏。