为什么 CSRF 防御必须配合 XSS 防御?
更新: 5/24/2026 字数: 0 字 时长: 0 分钟
这是 Web 安全里一条铁律:"有 XSS,CSRF 防御等于零"。要真正理解这句话,需要从 CSRF 防御的底层假设说起。
一、根本原因:CSRF 防御的安全基石建立在"同源策略"之上
所有 CSRF 防御方案(CSRF Token、SameSite Cookie、Origin 校验、双重提交 Cookie)的共同前提是:
恶意站点(evil.com)的 JavaScript 无法读取正规站点(bank.com)页面的任何内容。
这个"读不到"的保证,来自浏览器的同源策略(Same-Origin Policy)——它是 CSRF 防御能成立的唯一安全基石。
而 XSS 漏洞,恰恰是同源策略的合法绕过:XSS 让攻击者的代码直接运行在 bank.com 域内,此时同源策略不仅不会阻拦,反而会"保护"这段恶意代码——它拥有 bank.com 的完整权限。
地基塌了,上面盖什么楼都没用。
二、逐个防御方案,看 XSS 如何全部击穿
1. CSRF Token —— 直接被读走
正常防御逻辑:
- Token 嵌在
bank.com的 HTML 中(如<meta name="csrf-token" content="abc123">)。 - 恶意站点
evil.com因同源策略读不到这个 Token,伪造的请求里没有 Token,被拒。
有 XSS 时: 攻击者注入的脚本运行在 bank.com 域内,可以直接读取 Token:
// XSS 注入的代码,运行在 bank.com 域内
const token = document.querySelector('meta[name=csrf-token]').content;
// 或从隐藏表单读取
const token2 = document.querySelector('input[name=_csrf]').value;
// 拿着合法 Token,构造完美的"伪造"请求
fetch('/transfer', {
method: 'POST',
headers: { 'X-CSRF-Token': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ to: 'hacker', amount: 10000 })
});服务端校验:Cookie 有效 ✓,Token 正确 ✓ → 放行执行。CSRF Token 形同虚设。
2. SameSite Cookie —— 请求本身就是同站,根本不触发限制
正常防御逻辑:
SameSite=Lax/Strict让浏览器在跨站请求时不携带 Cookie。evil.com发往bank.com的请求是跨站,Cookie 不带,请求失败。
有 XSS 时: XSS 代码运行在 bank.com 域内,它发往 bank.com 的请求是完完全全的同站请求——SameSite 规则根本不适用,Cookie 正常携带。
// 这个请求的 Origin 就是 bank.com 本身,SameSite 拦不住
fetch('/transfer', { method: 'POST', credentials: 'include', ... });SameSite 防的是"跨站",而 XSS 已经让攻击者"住进了同站"——前提条件不成立。
3. Origin / Referer 校验 —— 请求来源就是合法域名
正常防御逻辑:
- 服务端校验
Origin/Referer必须是bank.com。 - 来自
evil.com的请求 Origin 不匹配,被拒。
有 XSS 时: XSS 脚本运行在 bank.com 页面里,它发起的请求 Origin 头就是 https://bank.com——完全合法,校验通过。
服务端无法区分"用户主动操作"和"XSS 脚本伪装的操作",因为从协议层看,两者一模一样。
4. 双重提交 Cookie —— Cookie 和参数都能读到
正常防御逻辑:
- Token 同时存在 Cookie 和请求参数中,服务端比对两者是否一致。
- 恶意站点能让浏览器带上 Cookie,但读不到 Cookie 内容,无法在参数里填入匹配值。
有 XSS 时:
- 如果 Cookie 没有
HttpOnly:直接document.cookie读出 Token。 - 即使 Cookie 有
HttpOnly:XSS 可以先发一个 GET 请求给某个会回显 Token 的接口(很多框架的 Token 接口就是这样),照样拿到。
// 即使 Cookie 是 HttpOnly,也能通过接口拿到 Token
fetch('/api/csrf-token').then(r => r.json()).then(data => {
fetch('/transfer', {
method: 'POST',
headers: { 'X-CSRF-Token': data.token },
body: '...'
});
});5. 自定义请求头 + CORS —— 同源请求无需预检
正常防御逻辑:
- 要求请求带
X-CSRF-Token等自定义头,跨域时触发 CORS 预检,恶意站点没有许可,预检失败。
有 XSS 时: 同源请求不触发 CORS 预检,自定义头可以随便加,攻击者畅通无阻。
6. 二次验证(验证码 / OTP) —— 唯一可能幸存的防线
这是唯一在 XSS 场景下仍可能有效的防御,但也只是"可能":
- 图形验证码:XSS 看不到图片内容(如果走 OCR/打码平台仍可绕过)。
- 短信 / 邮箱 OTP:XSS 拿不到用户手机或邮箱(除非站点把 OTP 回显到了同一页面)。
- U 盾 / 硬件密钥:XSS 无法操作硬件设备。
但即便如此,XSS 仍可以等待用户主动操作时劫持流程(如篡改转账金额和收款人后再让用户输验证码)。
三、关键认知:XSS 不是"绕过" CSRF 防御,而是"作废"了它
很多人会说"XSS 绕过了 CSRF 防御",这个说法不够准确。准确的描述是:
XSS 的存在,让 CSRF 防御的安全假设从一开始就不成立。CSRF 防御不是被"突破",而是被"作废"了。
类比一下:
| 防御措施 | 安全假设 | XSS 击穿方式 |
|---|---|---|
| 给保险柜上密码锁 | 别人不知道密码 | XSS = 攻击者就站在你身后看你输密码 |
| 给门装指纹锁 | 别人没有你的指纹 | XSS = 攻击者直接拿到了你的手 |
| 校验来访者身份证 | 别人办不到你的身份证 | XSS = 攻击者就是你本人在操作 |
所有 CSRF 防御方案,本质都是"让攻击者无法伪装成用户本人在本站点上的操作"。而 XSS 直接让攻击者就在本站点内、以用户本人的身份操作——伪装这一步都省了。
四、真实危害:有 XSS 的 CSRF 防御 = 0
没有 XSS 的站点:
- CSRF Token + SameSite Cookie 是非常坚固的防线,攻击成本极高。
有 XSS 的站点:
- CSRF Token 被读走 ✓
- SameSite Cookie 不适用 ✓
- Origin/Referer 校验通过 ✓
- 自定义请求头随便加 ✓
- 整套防御体系集体失效,攻击者拥有用户的全部权限。
更糟糕的是,XSS 不仅能做 CSRF 能做的所有事,还能做更多:读取页面、窃取数据、记录键盘、劫持会话、传播蠕虫……CSRF 能做的只是 XSS 能力的一个真子集。
五、防御优先级的实战结论
这就是为什么业界有这条铁律:
修复 XSS 的优先级,永远高于修复 CSRF。
实际防御应当遵循的顺序:
- 第一优先级:消除所有 XSS 漏洞(输入校验、输出转义、CSP、安全的模板引擎)。
- 第二优先级:部署 CSRF 防御(Token + SameSite + Origin 校验)。
- 第三优先级:纵深防御(Cookie 加 HttpOnly、敏感操作二次验证、WAF 监控)。
如果倒过来——XSS 不修、只修 CSRF——相当于门没装、只换了把好锁挂在地上,毫无意义。
六、一个有效的"安全气囊":CSP
由于 XSS 完全无法 100% 避免(业务复杂度决定了总有遗漏),业界推荐**额外部署 CSP(内容安全策略)**作为兜底:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
object-src 'none';
base-uri 'self';
report-uri /csp-violationCSP 的作用是:即使存在 XSS 注入点,恶意脚本也无法执行(因为不在白名单内)。这相当于给 CSRF 防御的"地基"加了一层钢筋——XSS 即使发生,破坏力也被极大限制。
七、一句话总结
CSRF 防御的所有方案,本质都依赖"同源策略"这一安全基石;而 XSS 是同源策略唯一合法的破坏者。 一旦存在 XSS,攻击者的代码就以"本人"身份运行在站点内,Token 能读、Cookie 能用、Origin 合法、SameSite 不适用——CSRF 防御不是被绕过,而是从一开始就不存在。所以 CSRF 防御必须配合 XSS 防御:前者保护身份不被冒用,后者保证身份不被夺取;没有后者,前者毫无意义。