用内容安全策略(CSP)防御 XSS 攻击:完整指南
内容安全策略(CSP)是防御 XSS 的最有效手段之一。从 default-src 入门,到 nonce/hash 处理内联脚本、strict-dynamic、report-only 灰度上线,一篇讲透 CSP 的配置与落地策略。
内容安全策略(Content Security Policy,CSP)本质上是一张资源加载白名单:通过 HTTP 响应头告诉浏览器"本页面允许从哪些地方加载哪些资源"。它的默认逻辑是"默认拒绝"——没被列入白名单的资源,浏览器直接阻止加载。
为什么 CSP 能防住 XSS
XSS 攻击的本质是攻击者把恶意 JavaScript 注入你的页面,让它以你网站的权限运行(读 Cookie、伪造请求、篡改页面)。CSP 的防御逻辑釜底抽薪:哪怕攻击者成功把 <script src="https://evil.com/x.js"> 注入了 HTML,只要你的策略没把 evil.com 列入 script-src,浏览器就拒绝加载并记录违规。
CSP 标准历经三个版本:Level 1 建立基础防护,Level 2 引入 nonce 和 hash 处理内联脚本,现行的 Level 3 加入 strict-dynamic 等更精细的控制。所有现代浏览器均已支持。
CSP 基础:指令与来源
CSP 通过 Content-Security-Policy 响应头下发,内容是若干条"指令",每条指令管一类资源:
Content-Security-Policy: default-src 'self'; img-src *; script-src 'self' trusted.cdn.com;
default-src 'self':兜底规则——没单独声明的资源类型,默认只允许同源加载。img-src *:图片允许任意来源(*通配)。script-src 'self' trusted.cdn.com:JS 只允许本域和指定的可信 CDN。
来源的写法:'self'(同源)、'none'(全部禁止)、具体域名、*.example.com(通配子域)、以及按 scheme(https:)。
两种模式:强制执行与仅报告
CSP 有两个响应头,对应两种模式:
| 模式 | 响应头 | 行为 |
|---|---|---|
| 强制执行 | Content-Security-Policy |
违规资源被阻止加载 |
| 仅报告 | Content-Security-Policy-Report-Only |
不阻止,只上报违规记录 |
仅报告模式是灰度落地的关键:先只收集"如果启用会拦什么",把策略调好再真正启用,避免一把上线把正常资源全拦了。
配置你的第一条 CSP
从严格到宽松逐步放行:
Content-Security-Policy: default-src 'self';
最简单也最严格——所有资源仅同源。对纯静态小站够用,但现代站点几乎都要放行一些外部资源:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://analytics.example.com;
style-src 'self' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
Express 里加一个中间件即可下发:
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' https://analytics.example.com; style-src 'self' https://fonts.googleapis.com;"
);
next();
});
常用指令速查
| 指令 | 管控对象 | 典型配置 |
|---|---|---|
default-src |
所有类型的兜底 | 'self' |
script-src |
JavaScript | 'self' + 可信 CDN,避免 'unsafe-inline' |
style-src |
CSS | 'self' + 字体服务 |
img-src |
图片 | 'self' data: https: |
connect-src |
fetch/XHR/WebSocket | 'self' + 你的 API 域 |
font-src |
字体 | 'self' + 字体 CDN |
object-src |
Flash/Java 插件 | 'none'(现代站点都应禁掉) |
frame-ancestors |
谁能 iframe 嵌你 | 'none' 或指定域(防点击劫持) |
base-uri |
<base> 标签 |
'self' |
form-action |
表单提交目标 | 'self' |
进阶:内联脚本、nonce 与 strict-dynamic
默认情况下 CSP 禁止一切内联 <script>——这正是防注入的关键,但现实项目里内联脚本常常存在。三种解法,安全性递增:
-
'unsafe-inline':放行所有内联脚本。方便但基本自废武功,别在生产用。 -
nonce(一次性随机值):服务器每次响应生成随机数,只放行带匹配 nonce 的脚本:
Content-Security-Policy: script-src 'nonce-r4nd0mV4lue';<script nonce="r4nd0mV4lue">/* 可信的内联脚本 */</script>攻击者注入的脚本没有正确 nonce,被浏览器拒绝。
-
strict-dynamic:信任"由可信脚本加载的脚本",适合现代前端(打包器动态插入脚本):Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic';
另一个实用指令是 upgrade-insecure-requests:让浏览器自动把页面里的 http:// 资源请求升级为 https://,混合内容问题一键缓解。
违规报告与监控
CSP 最有价值的副产品是违规报告:浏览器把每次违规 POST 到你指定的端点:
Content-Security-Policy: default-src 'self'; report-uri /csp-violations;
报告里包含被拦截的资源 URL、所在页面、违反的指令——它既是策略调优的输入,也是一个免费的 XSS 攻击探测器:线上突然出现的违规报告高峰,往往意味着有人正在尝试注入,或者某次发布引入了不合规资源。
落地策略:别一步到位
推荐的实施路径:
- 仅报告模式上线:
Report-Only+ 宽松策略,收集一周违规数据; - 按数据收紧:把合法来源加进白名单,直到"正常流量零违规";
- 切换为强制执行;
- 持续监控报告:作为安全信号长期保留。
现代框架的注意点:SPA 要把 API 域名加进 connect-src;WebSocket 归 connect-src 管(wss: 来源单独声明);第三方组件(客服挂件、验证码)各要一条来源——评估第三方脚本时,CSP 白名单本身就是一份"谁在页面上跑代码"的清单。
观测云对照
CSP 违规报告本质上是浏览器端产生的安全日志。观测云 RUM 采集真实用户侧的前端数据,结合自定义上报可以把 CSP 违规与页面性能、JS 错误放在同一视图分析;违规上报端点的日志接入观测云后,可对违规量突增配置监控告警——攻击尝试或发布引入的资源问题都能第一时间感知。
常见问题(FAQ)
Q:上线 CSP 后网站部分功能挂了?
A:说明你跳过了 report-only 阶段。先切回仅报告模式,从违规报告里找出被误拦的资源补进白名单,再重新强制。
Q:CSP 能完全替代输入过滤/转义吗?
A:不能。CSP 是纵深防御的一层,不是免死金牌。输出转义、参数化查询、HttpOnly Cookie 这些基本功仍然要做。
Q:report-uri 和 report-to 用哪个?
A:report-to 是新标准(配合 Report-To 头),report-uri 老但兼容性好。过渡期两个都写。