Skip to content

为什么 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:

javascript
// 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 形同虚设。

正常防御逻辑

  • SameSite=Lax/Strict 让浏览器在跨站请求时不携带 Cookie。
  • evil.com 发往 bank.com 的请求是跨站,Cookie 不带,请求失败。

有 XSS 时: XSS 代码运行在 bank.com 域内,它发往 bank.com 的请求是完完全全的同站请求——SameSite 规则根本不适用,Cookie 正常携带。

javascript
// 这个请求的 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 脚本伪装的操作",因为从协议层看,两者一模一样。

正常防御逻辑

  • Token 同时存在 Cookie 和请求参数中,服务端比对两者是否一致。
  • 恶意站点能让浏览器带上 Cookie,但读不到 Cookie 内容,无法在参数里填入匹配值。

有 XSS 时

  • 如果 Cookie 没有 HttpOnly:直接 document.cookie 读出 Token。
  • 即使 Cookie 有 HttpOnly:XSS 可以先发一个 GET 请求给某个会回显 Token 的接口(很多框架的 Token 接口就是这样),照样拿到。
javascript
// 即使 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。

实际防御应当遵循的顺序:

  1. 第一优先级:消除所有 XSS 漏洞(输入校验、输出转义、CSP、安全的模板引擎)。
  2. 第二优先级:部署 CSRF 防御(Token + SameSite + Origin 校验)。
  3. 第三优先级:纵深防御(Cookie 加 HttpOnly、敏感操作二次验证、WAF 监控)。

如果倒过来——XSS 不修、只修 CSRF——相当于门没装、只换了把好锁挂在地上,毫无意义。

六、一个有效的"安全气囊":CSP

由于 XSS 完全无法 100% 避免(业务复杂度决定了总有遗漏),业界推荐**额外部署 CSP(内容安全策略)**作为兜底:

http
Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'nonce-{random}'; 
  object-src 'none';
  base-uri 'self';
  report-uri /csp-violation

CSP 的作用是:即使存在 XSS 注入点,恶意脚本也无法执行(因为不在白名单内)。这相当于给 CSRF 防御的"地基"加了一层钢筋——XSS 即使发生,破坏力也被极大限制。

七、一句话总结

CSRF 防御的所有方案,本质都依赖"同源策略"这一安全基石;而 XSS 是同源策略唯一合法的破坏者。 一旦存在 XSS,攻击者的代码就以"本人"身份运行在站点内,Token 能读、Cookie 能用、Origin 合法、SameSite 不适用——CSRF 防御不是被绕过,而是从一开始就不存在。所以 CSRF 防御必须配合 XSS 防御:前者保护身份不被冒用,后者保证身份不被夺取;没有后者,前者毫无意义。