在实际业务里摸爬滚打过一段时间后,我对CSRF这个老牌漏洞有了些新的理解。今天不聊那些嘴上说烂的“Token校验、SameSite”空话,而是用一篇完整的教程,从底层机制到实际场景,把CSRF这个看似简单的漏洞讲透彻。无论你是刚入行的新人,还是写过几年业务代码的开发,我相信这篇文章都能给你一些不一样的启发。
1. 内容整体设计与思路拆解
1.1 为什么说CSRF“入门容易,精通难”
CSRF(Cross-Site Request Forgery,跨站请求伪造)在各类漏洞排行里一直没掉出过前排,但很多零基础的安全爱好者在学习它时,会经历这样一个过程:刚看教程时觉得“这还不简单?”,无非就是诱导用户点击一个链接,就能伪造请求。真正上手测试后才发现,实际情况复杂得多——CSRF漏洞的利用成功与否,往往取决于你对目标站点的业务逻辑理解有多深,而不是仅仅会构造一个表单就完事。
我记得团队里一个新人A同学,第一次接触CSRF时,对照网上模版写了一个恶意的提交表单,兴冲冲地跑去测试某个内部系统。结果显而易见,请求被拦截了。他当时疑惑地问我:“是不是目标系统没这个漏洞?”我让他去抓包看下实际请求里有没有Token参数,再看看Referer头部是怎么处理的。结果发现,目标系统不仅校验了Token,还把SameSite属性设置成了Lax。这个案例很典型,让我意识到:CSRF的“简单”只停留在教科书层面,真实环境下CSRF最难的其实是“绕过”与“防御”之间的博弈。
1.2 这期教程的价值与适合人群
这篇文章的定位不是一篇“读起来很爽”的漏洞列表,而是一篇具备实操参考价值的挖掘指南。我会从零开始拆解CSRF的触发链路,逐层分析“同源策略”“表单提交”“JSON跨域”这些看似基础,但综合起来能决定漏洞是否存在的因素。
如果你正在学习Web安全,或者你是后端开发、全栈工程师,想从“防御者视角”反向理解为什么CSRF会让开发团队头疼,这篇文章很适合你。我会用自己的实战经历、模拟测试环境和测试中踩过的坑,给你展示一个符合真实业务场景的CSRF测试全流程。
提示:所有测试操作均应在具备合法授权的前提下进行。不建议对未经许可的目标发起任何安全测试,否则后果自负。
2. 核心机制解析:CSRF为什么能够存在
2.1 先从“会话”聊起:Cookie与身份认证
想要理解CSRF,必须先理解Web应用是如何识别一个用户的。HTTP协议本身是无状态的,服务器无法直接知道“当前发起请求的人是谁”。为了让用户不需要每次请求都重新输入用户名密码,Web应用引入了“会话(Session)”机制,通常通过Cookie在客户端保存一个“会话标识”(Session ID)。你登录了一个网站,服务器生成一个Session ID存在你浏览器的Cookie里,并把它和你的用户身份绑定在一起。之后你的每次请求带上这个Cookie,服务器确认无误后,就认为“这是本人在操作”。
这个机制本身没有问题,但它催生了一个盲区:浏览器自动携带Cookie的特性。
2.2 攻击的核心逻辑:浏览器的“无脑”行为
CSRF的攻击逻辑,是建立在浏览器“自动携带身份凭据”这一行为之上的。攻击者构造一个恶意页面,页面里“藏”着一个指向目标网站的请求。当受害者访问这个恶意页面时,浏览器会忠实地替受害者发出那个请求,并且只要受害者在目标网站处于登录状态,浏览器就会自动附带上他的Cookie。
举个最常见的例子,假设你在某个论坛保持了登录状态,黑客在某个人气很旺的图片分享帖里嵌入了一个隐藏表单,表单的提交地址是那个论坛的“修改邮箱”接口。当你打开这个帖子时,隐藏表单自动提交,你的邮箱就被修改成了黑客的邮箱。整个过程你完全无感知,请求已经发出,并且携带了你的合法身份凭证。这,就是CSRF最核心的模型。
2.3 关键误区:CSRF与XSS的本质区别
很多零基础同学会把CSRF和XSS(跨站脚本)弄混,这里我用一句话说清楚:
- XSS:利用的是“用户对网站的信任”,核心是“在目标网站内执行恶意脚本”。
- CSRF:利用的是“网站对用户浏览器的信任”,核心是“在受害者的浏览器里发起带有身份的伪造请求”。
在CSRF攻击中,攻击者完全不需要拿到目标网站的Cookie、Session ID或其他任何身份凭证,攻击者只需要知道请求的目标URL和参数格式。
2.4 CSRF利用成功的条件(缺一不可)
在实际测试中,一个CSRF漏洞想要利用成功,通常需要满足以下三个条件之一或全部:
- 目标请求必须使用Cookie或浏览器自动携带的身份进行鉴权。如果目标接口改用Token放在请求头(比如Authorization: Bearer),那么纯CSRF攻击基本行不通。
- 目标请求没有防伪校验。比如没有CSRF Token、没有校验Referer/Origin,或者校验方式可以被绕过。
- 攻击者能够构造一个“命中”的请求。这意味着攻击者要精确知道目标接口的URL、请求方法(POST/GET)、必要的参数名和参数值。
理解了这个链路,你才能明白,为什么CSRF漏洞的高发区往往是**“非核心业务但涉及状态变更”**的接口:比如修改个人资料、绑定手机号、修改密码(一定条件下)、点赞、关注、发表评论等。这些接口往往安全防护松懈,但又真实地改变了用户数据。
3. 实操要点:从零构造一个CSRF验证环境
以“教”带“学”是最快的成长路径。想要真正掌握CSRF漏洞的利用和防御,自己动手搭一个靶场环境是必经之路。我分享一下我平时测试用的最小化环境方案,简单、直接、不依赖外部网络。
3.1 环境准备:三个组件,缺一不可
你需要准备这三个角色:
- 一个目标站点:我建议用本地PHPStudy或者直接写一个最简单的PHP/Flask/Express服务,提供登录鉴权接口和“修改个人信息”接口。
- 一个攻击者站点:同样可以放在本地,或者使用本地文件(file://协议模拟跨域),注意文件协议在某些浏览器里Cookie行为不同,建议还是起一个本地HTTP服务。
- 一个受害者浏览器:推荐使用无痕窗口,方便模拟登录态。
3.2 核心靶场代码:目标接口设计
我以Python Flask为例,写一个最典型的“修改昵称”接口,用于演示。这个接口仅依赖Cookie进行身份识别,没有CSRF防护,为了模拟真实性,我特意让它使用POST请求,内容编码格式为表单(application/x-www-form-urlencoded):
from flask import Flask, request, make_response, session app = Flask(__name__) app.secret_key = 'test-secret-key' # 仅演示用 users = { 'admin': {'password': '123456', 'nickname': 'Admin'}, } # 登录接口,设置Session @app.route('/login', methods=['POST']) def login(): username = request.form.get('username') password = request.form.get('password') user = users.get(username) if user and user['password'] == password: session['username'] = username return make_response('login ok') return make_response('login failed', 403) # 修改昵称接口 —— 仅检查登录状态,无CSRF Token @app.route('/update_nickname', methods=['POST']) def update_nickname(): if 'username' not in session: return make_response('not logged in', 401) new_nickname = request.form.get('nickname') # 实际业务在这里更新数据库 users[session['username']]['nickname'] = new_nickname return make_response(f"nickname updated to {new_nickname}") if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)这段代码里,update_nickname接口没有Token校验,逻辑极其简单——只判断当前浏览器是否携带了有效Session Cookie。
3.3 攻击者页面:隐藏表单与自动提交
接下来是攻击者的阵地。我需要构造一个“恶意页面”,这个页面放在另一个本地端口(比如8001)上。关键点是,这个页面里“藏”的表单提交地址是目标站点(127.0.0.1:8000)的接口,并且由于表单提交天然支持跨域,浏览器不会拦截这种“页面发起跨域请求”的行为。
<html> <body> <h1>一个正常的私人博客页面</h1> <p>如果你看到了这篇文章,说明你已经中招了。</p> <form id="csrf-form" method="POST" action="http://127.0.0.1:8000/update_nickname" enctype="application/x-www-form-urlencoded"> <input type="hidden" name="nickname" value="Hacked-By-CSRF" /> </form> <script> // 页面加载完成后立即提交 window.onload = function() { document.getElementById('csrf-form').submit(); } </script> </body> </html>页面看起来人畜无害,但实际上是一个“请求发射器”。任何登录了目标站点的用户,只要访问了这个恶意页面,浏览器就会向目标接口发出一个POST请求,并把受害者的Session Cookie一并带上。
3.4 完整复现与现象解释
操作步骤很简单,我按顺序演示一遍:
- 打开无痕窗口A,访问
http://127.0.0.1:8000/login,提交用户名和密码(admin/123456),确认登录成功。 - 登录后访问任意页面确认Cookie已被设置,此时浏览器对目标站点是“已认证”状态。
- 另开一个无痕窗口B(确保和目标站点没有直接关联),访问攻击者控制的恶意页面
http://127.0.0.1:8001/evil.html。 - 观察窗口A(或者直接查询数据库中的nickname字段),你会发现,即使没用目标站点的Cookie、没有输入密码,用户的昵称也已经被修改成了
Hacked-By-CSRF。
现象背后发生了什么?窗口B的浏览器向目标服务器发送了一个携带了SessionID的合法POST请求,目标服务器只认Cookie,不关心这个请求到底是不是用户本人主动发出的,于是直接更新了数据。受害者完全没有感知。
这里补充一个容易被忽略的细节:为什么表单提交能跨域?因为 HTML 表单(form)的
action属性可以指向任意域名的地址,浏览器出于历史原因(老式的Web交互模式),允许这种POST跨域行为。而XMLHttpRequest / fetch 则受同源策略限制,无法直接跨域发送“可携带Cookie”的请求(需要额外配置CORS)。这也是CSRF攻击最传统的载体是“表单”的根本原因。
4. 常见问题与排查技巧:为什么我的测试不成功?
实际测试中,不可能每次CSRF利用都一击即中。我把近几年测试中遇到的高频失败原因做了汇总,你对照着排查基本能解决95%的问题。
4.1 防伪Token校验导致的失败
现象:提交恶意表单后,对方返回“请求无效”或“参数校验失败”。
排查思路:这是最常见的拦截手段。目标接口在渲染时生成了一个一次性Token,存入隐藏字段或自定义Header,下一次提交时对比是否一致。
这里有个关键测试思路:不要一上来就说“这里防御了,没法绕过”。你要在合法业务里找“Token是否被复用、是否可预测、是否在特定接口缺失”的场景。比如:
- 注册接口的新用户Token是否固定?
- 某些接口(如退订邮件、消息已读上报)是否因为历史原因漏掉了Token?
- GET类型请求也能触发状态变更且无Token时,能否利用图片标签
<img src="http://target.com/delete?id=123">直接触发?
4.2 SameSite Cookie策略导致的失败
现象:写好的利用页面,请求始终不携带Cookie,抓包看Cookie字段是空的。
排查思路:这是现代浏览器抵御CSRF的标准手段之一。SameSite属性有Strict、Lax、None三个值,简单说:
Strict:完全禁止第三方Cookie随跨站请求发送。Lax:只有“顶层导航”的GET请求会携带Cookie,表单POST和异步请求都不会携带。None:允许所有请求携带Cookie,但需要在HTTPS环境配置。
如果目标站点的Cookie设置的是SameSite=Lax或Strict,传统的表单自动提交方式大概率失效。这时候你得转换测试思路:尝试寻找“子域漏洞+Cookie域配置宽松”的组合,或者看看是否有“JSONP”“CORS配置错误”可以利用的服务端逻辑。
4.3 Referer/Origin校验带来的挑战
现象:目标服务器校验了Referer或Origin头,发现请求来源不是自身域名,直接拒绝。
排查思路:如果Referer校验是通过“包含匹配”(例如检查请求头中是否包含了某个域名片段),那么就存在绕过空间。
举个例子,目标校验Referer中是否包含target.com,那攻击者可以把恶意页面放在target.com.attacker.com域名下(不同子域名),Referer就会变成http://target.com.attacker.com/evil-page,包含目标域名但实际是攻击者控制的。不过这种方式需要攻击者拥有一个可控域名的子域解析,在真实SRC挖掘中,属于高危接口+可利用边缘场景的结合。
4.4 JSON接口与Content-Type校验
现象:目标接口要求Content-Type: application/json,而HTML表单只能发送text/plain、application/x-www-form-urlencoded、multipart/form-data这三种编码,无法直接提交JSON数据。
排查思路:碰到这种情况,传统的form提交失效。可以试试这两个方向:
- JSONP接口:如果目标站点存在返回JSON数据的Callback接口(老系统常见),可以构造
<script src="...">跨域加载并执行特定函数,间接向接口发送请求。 - Flash / 表单跨域漏洞:2010年前后的老系统内部处理函数,有的会把
text/plain主体按JSON解析(因为request.getParameter无法读取,但request.getInputStream是通的),这种情况纯CSRF攻击也有可能成立。
4.5 调整请求方式:GET型CSRF别忽略
现象:目标接口只支持GET请求,但按照“传统CSRF教程”,好像都需要POST表单。
排查思路:如果GET请求直接携带了敏感操作参数,例如DELETE /index.php?id=2或者POST 接口支持 GET 方式访问(很多开发框架为了方便调试默认允许),这种场景下用<img>、<link>、<script>标签就能触发跨站请求,根本不需要表单。
为什么不建议只测POST?因为“能用GET绝不用POST”的老一代开发习惯,以及RESTful接口的普遍化,导致“GET方法修改状态”的案例至今没有断绝。这是一个非常典型的“低投入高回报”测试点。
5. 防御方案与有效性对比
说完了攻击侧和测试侧,我站在开发者的视角,聊几套CSRF防御方案的实际效果。记住,没有银弹,安全的本质是“成本与收益的博弈”。
5.1 同步令牌模式(Synchronizer Token Pattern)
思路:服务器生成随机Token,写入表单隐藏字段或响应Header中,并在Session中保存一份。每次请求提交时,比对两个值是否一致。
有效性与缺点:这是最经典的CSRF防御机制,有效防止“无脑表单提交”。但它的防护强度取决于:
- Token是否绑定用户身份(全局Token在“登录前功能”上容易被绕过)
- Token是否在每次请求后刷新(一次性Token比固定Token安全得多)
- Token的随机性(生成算法是否安全)
实际踩坑时你会发现,很多系统中的Token只做了“存在性校验”,没做“一致性校验”,或者虽然做了校验,但Token同时存在于Cookie中导致可以配合XSS缺陷绕过。
5.2 双重提交Cookie(Double Submit Cookie)
思路:服务器要求前端提交一个Token,这个Token在客户端以Cookie形式存在。服务器校验“提交的Token”和“Cookie里的Token”是否一致,由于跨站请求无法设置目标域名的Cookie,所以认为安全的。
有效性与缺点:这个方案在“纯CSRF”场景下很有效,但如果目标环境存在子域任一处XSS,或者Cookie由于某些配置可以被子域影响(例如Cookie Domain设置太宽),攻击者可以在攻击者域名下写入恶意Cookie来伪造提交Token,导致绕过。
5.3 SameSite Cookie 属性
前面已经提到过,这是浏览器层面提供的原生防护。强烈建议开发团队将所有“不要求跨站携带”的Cookie一律设置为SameSite=Lax,必要时用Strict。在Chrome和Firefox高版本中,这基本能把传统CSRF的90%问题堵在门外。这也是2020年以后CSRF漏洞数量陡降的主因之一。
5.4 Origin/Referer校验
思路:如果请求头中的Origin不是本站域名,则拒绝。这种方案部署成本低,但依然存在“空Origin”通过(比如沙箱页面、内嵌页面、某些旧版浏览器)的情况。建议配合Token方案做双保险。
5.5 防御方案对比速查表
| 防御方案 | CSRF防护强度 | 部署成本 | 绕过难度 | 备注 |
|---|---|---|---|---|
| Token(同步令牌) | 高 | 中 | 高 | 需注意Token生成算法和失效机制 |
| 双重提交Cookie | 中 | 低 | 中 | 对子域XSS较敏感 |
| SameSite属性 | 高 | 极低 | 高(主流浏览器) | 不能完全替代服务端防御,兼容性问题 |
| Referer/Origin校验 | 中 | 低 | 中 | 需关注空值和子域解析问题 |
| 验证码/二次确认 | 极高 | 高 | 极高 | 常用于高敏感操作,如改密、转账 |
我的建议是:核心业务采取“Token+SameSite”组合,重要操作(修改密码、绑定手机、转账)额外增加二次验证。在成本和体验之间寻求一个可控的平衡点。
6. 从漏洞挖掘角度看CSRF的行业变化
6.1 “降级”为高危链里的一环
这两年CSRF在独立漏洞挖掘中的地位有所下降,原因正如前面提到的:浏览器对Cookie的跨站限制越来越严格,开发者框架默认启用防伪校验,独立触发一个“纯CSRF”漏洞的难度确实大了。
但注意,CSRF并没有消失,而是更多作为“高阶漏洞链条”中的一环出现。比如在以下场景中,CSRF依然是关键点睛之笔:
- 配合XSS降级利用:一个理论上只能自我触发的存储型XSS,通过CSRF将数据写入目标用户,扩大影响范围。
- 配合并发逻辑漏洞:利用CSRF让受害者在不知情的情况下启动某个操作(如绑定攻击者账号、关注特定用户、上传特定资源),配合竞态条件实现“双重操作”。
- 攻击“边缘业务”:很多大公司在核心业务边边角角的“活动页面”“埋点上报”“第三方登录回调”里,依然存在CSRF。这些接口数据价值不高,但一旦涉及“修改回调地址”或者“绑定手机号”就可能是高分值漏洞。
6.2 零基础学习的建议:不要为了“挖洞”而“挖洞”
很多同学学习安全时容易陷入一种“刷漏洞”的心态。作为一个过来人,我的建议是先把一条完整链路的底层原理摸透。为什么表单能跨域?为什么浏览器要默认携带Cookie?为什么SameSite设置为None时容易出问题?如果一个漏洞的原理你已经能够用自己的语言向别人解释清楚,那你离真正掌握它就不远了。
同时,一定要熟悉抓包工具的使用。无论是Burp Suite还是浏览器开发者工具,你需要从“结果”反推“过程”,观察请求的来龙去脉。我见过很多初学者,明明漏洞已经触发了,却因为看不懂请求面板里的参数来源,错失了确认漏洞的机会。
6.3 合法测试的边界
最后必须非常严肃地强调一遍:本文所有内容仅为技术交流与教育目的。每一个涉及漏洞的工具、思路、代码片段都应当在合规授权的范围内使用。企业授权测试和公益SRC有明确测试范围——在范围外测试,即使没造成破坏,也是违背法律和道德的行为。做安全,技术重要,边界感更重要。这一点,每个从事安全领域的人都应该牢记。
现在可以打开你身边的靶场,用本地环境按照上面的演示手动复现一遍CSRF的完整攻击和防御流程。这比刷一百个漏洞报告都管用。