
2026-09-02 · 阅读 1
XSS和CSRF
一次讲透 XSS 与 CSRF:从攻击原理到项目实战防御
这篇文章源自我自己在做一个全栈项目(Next.js + NestJS)时踩坑 + 面试被问爆之后的系统复盘。以前这两个概念我总是背了就忘、答题时还会串台,后来发现只要抓住"攻击者到底拿到了什么"这一条主线,整个知识体系就自动立起来了。希望这篇能帮你一次记住,不再混淆。
0. 先用一句话分清两者
- XSS(跨站脚本攻击):攻击者把恶意代码伪装成数据塞进网页,浏览器把它当代码执行了——数据被当作代码。
- CSRF(跨站请求伪造):攻击者从头到尾拿不到你的 Cookie,他只是借用了"浏览器发请求会自动带 Cookie"这个机制,让服务器误以为是你在操作——冒名顶替。
口诀:XSS = 植入内鬼,在页面里执行代码、偷东西;CSRF = 冒名顶替,不偷东西,借浏览器自动带 Cookie 的身份。
1. XSS:数据被当作代码
1.1 为什么会发生
本质原因只有一个:输出时没有正确转义/过滤,导致浏览器分不清"这是用户想说的话,还是要执行的命令",把数据当成了代码。
1.2 三种攻击类型(一张表判定)
| 类型 | 恶意代码存在哪里 | 触发条件 | 典型场景 |
|---|---|---|---|
| 反射型 | URL 参数里 | 服务器把参数原样回显到页面 | 搜索结果页回显关键词、错误页回显 URL |
| 存储型 | 数据库里(评论、昵称、文件名) | 任何用户打开页面就执行 | 论坛评论区植入脚本,所有看帖的人中招 |
| DOM 型 | URL 的 hash 等前端可读处 | 前端 JS 自己用 innerHTML 插进去 | 前端读 location.hash 直接塞进 DOM |
判定口诀:看恶意代码存在哪里——存数据库 → 存储型;在 URL 参数、靠服务器回显 → 反射型;在 URL hash、靠前端 JS 自己插 → DOM 型。
一个容易判错的例子:文件名场景。用户上传的文件叫 <img src=x onerror=alert(1)>.jpg,文件名被存进数据库、被渲染到文件列表页——这是存储型,不是反射型!因为恶意代码存在数据库里。
1.3 四层防御(面试按这个顺序答,层层递进)
- 输出转义(主防):JSX 的
{file.name}渲染时 React 自动转义,永远只是文字; - 不用危险 API:文件名/用户输入不塞
dangerouslySetInnerHTML/innerHTML;富文本要用 DOMPurify 之类的库先消毒; - HttpOnly(保底):就算 XSS 真的执行了,
document.cookie也读不到,Token 偷不走; - CSP(兜底):响应头声明"只允许执行白名单来源的脚本",内联脚本直接拒绝执行——XSS 就算发生了也跑不起来。
React 两行对照:
<div>{file.name}</div> // ✅ 自动转义,永远是文字
<div dangerouslySetInnerHTML={{ __html: userContent }} /> // ❌ 危险API,等于手动关掉保护
1.4 我项目里的实际方案
我的项目采用 JWT + GitHub OAuth 双重认证,通过 HttpOnly Cookie 存储 Token 来减小 XSS 的伤害:
- 浏览器每次请求会自动带上这个 Cookie;
- 但页面里的 JS 用
document.cookie读不到它; - 所以就算 XSS 发生了,恶意脚本也偷不走 Token。
注意措辞:HttpOnly 是减小伤害,不是阻止攻击本身——脚本照样能执行(还能钓鱼、键盘记录),只是偷不走凭证。这句话在面试里说出来,比背十个概念更能体现你理解到位。
2. CSRF:借刀杀人,刀是浏览器自动带 Cookie 的机制
2.1 攻击原理
CSRF 的完整链条是:攻击者拿到了什么?——什么都没拿到。他只是诱导已登录用户访问 evil.com,页面里藏着一个:
<!-- evil.com 上的钓鱼页 -->
<img src="https://mybank.com/transfer?to=hacker&amount=10000" />
浏览器加载这张"图片"时,对 mybank.com 发起 GET 请求,并且自动带上 mybank.com 的 Cookie(浏览器的同源策略不拦截"发请求",只拦截"读响应")。服务器只认 Cookie 不认人,于是转账成功。
2.2 三种防御手段
- SameSite Cookie:设置
SameSite=Lax/Strict,只有站内正常导航才带 Cookie。evil.com 的 img 请求一到,Cookie 根本不会跟过去——攻击链条断在第一步; - CSRF Token:服务器给表单/页面埋一个随机 Token,提交时必须原样带回。evil.com 因为同源策略读不到你页面的内容,所以伪造不了 Token;
- Token 放请求头而不是 Cookie:JWT 放
Authorization头意味着每个请求都要前端代码主动携带,而 evil.com 的<img>/<form>标签没有能力伪造自定义请求头——CSRF 天然免疫。
2.3 Token 放哪?一张镜像取舍表
这是很多文章不讲、但面试官爱追问的点:Header 和 Cookie 的安全模型互为镜像。
| 放置位置 | 防 CSRF | 防 XSS 偷取 |
|---|---|---|
Authorization 请求头 | ✅ 天然免疫(标签带不了自定义头) | ❌ JS 能读到,怕 XSS |
Cookie + httpOnly | ❌ 谁发的都带,怕 CSRF | ✅ JS 读不到,防偷 |
所以没有完美方案,只有组合拳:放 Header 就要严防 XSS;放 Cookie 就必须加 sameSite 防 CSRF。
2.4 项目里的一行三防
我的项目(NestJS)里就是这一行配置,三个参数各防一件事:
res.cookie('token', token, {
httpOnly: true, // 防 XSS 偷:JS 读不到
sameSite: 'lax', // 防 CSRF:第三方请求不带
secure: true, // 只走 HTTPS:明文传输防中间人
})
记忆钩子:每个"所以"都接在它的病后面——谁发的都带 → sameSite;JS 能偷 → httpOnly;明文传输 → secure。
3. XSS vs CSRF 终极对照表
| 维度 | XSS | CSRF |
|---|---|---|
| 攻击本质 | 植入代码,页面里执行 | 冒名顶替,借 Cookie 身份 |
| 攻击者拿到什么 | 页面执行权(能偷能改) | 什么都没拿到,只借自动带 Cookie 的机制 |
| 漏洞根源 | 输出未转义,数据被当代码 | 服务器只认 Cookie 不认请求来源 |
| 触发方式 | 打开被污染的页面 | 诱导用户点开不明链接/访问恶意页 |
| 核心防御 | 输出转义 + 危险 API 禁用 | SameSite + CSRF Token |
| 兜底防御 | HttpOnly(减小伤害)+ CSP | 校验 Origin/Referer |
| 组合拳 | XSS 成功后可直接发起 CSRF | —— |
关于"冒名操作"归属的澄清(我曾经混淆的点):冒名是 CSRF 的唯一手段,但只是 XSS 的众多后果之一。说 XSS 危害时,说三件套就够:偷 Cookie、篡改页面钓鱼、键盘记录,最后可以补一句"XSS 成功后还能直接发起 CSRF 打组合拳"——这句是加分项。
4. 30 秒面试版答案(附彩蛋)
被问"讲讲 XSS"时:
XSS 是攻击者把恶意代码伪装成数据塞进网页,浏览器把它当代码执行了。它有三种类型:存储型(存数据库)、反射型(URL 参数回显)、DOM 型(前端 JS 自己插入)。防御上我项目里做了输出转义、禁用危险 API,并用 HttpOnly Cookie 存储 Token 做保底,再加 CSP 兜底。
主动补的一句(展示概念清晰):
而且 XSS 和 CSRF 能打组合拳——XSS 偷不到 HttpOnly Cookie,但脚本本身可以在页面里直接发起 CSRF 请求,所以两层防御缺一不可。
5. 总结记忆卡
- XSS 一句话:数据被当作代码执行
- CSRF 一句话:攻击者拿不到 Cookie,只借浏览器自动带的机制冒名操作
- XSS 三类型判定:看恶意代码存在哪(数据库/URL参数回显/URL hash前端插)
- XSS 四层防御:转义 → 危险API → HttpOnly → CSP(主防→保底→兜底)
- Token 取舍镜像:Header 怕偷,Cookie 怕伪造
- 一行三防:httpOnly 防偷,sameSite 防带,secure 走 HTTPS
本文基于个人项目实战与面试复盘整理,如有疏漏欢迎指出。