
做web渗透这些年我越来越觉得CSRF漏洞被严重低估了。它不像SQL注入那样能把整个数据库拖出来摆在你面前也不像XSS那样能弹个框让你直观感受到被入侵但CSRF往往藏在你最意想不到的业务逻辑里——一个链接、一张图片、一次自动提交的表单就能在用户毫无感知的情况下完成改密、换绑、转账这类高敏感操作。这篇文章我想把这几年在授权测试里遇到CSRF的实战体会完整梳理一遍从漏洞成因、攻击形态到发现与验证的完整流程再到我理解中真正能落地的深度防御体系。无论你是刚入门web渗透的新手还是被CSRF防护搞得头疼的开发同学这篇都能给你一套可以直接拿去用的思路。1. 从一次点链接被改邮箱的模拟开始CSRF的攻击路径复盘1.1 一次完整的CSRF攻击推演先看一个我经常在攻防演练里演示的场景。假设目标系统example.com有个非常常见的接口用户修改绑定邮箱的请求长这样GET /user/changeEmail?emailattackerevil.com HTTP/1.1 Host: example.com Cookie: sessionidabc123注意几个细节这是GET请求、敏感操作带了session Cookie、服务端没有校验任何token或来源。攻击者只需要把这个URL改写成一张图片或一个链接img srchttps://example.com/user/changeEmail?emailattackerevil.com width0 height0 /然后把这段内容放到受害者会访问的地方论坛帖子、聊天窗口、个人博客哪怕是一封伪装成通知的邮件。受害者只要登录着example.com再点开这个页面浏览器就会自动向example.com发出那个改邮箱的请求并自动携带sessionid Cookie。服务端收到请求发现你有合法会话参数也齐全直接执行。接下来攻击者顺着找回密码流程用这个新邮箱接收重置链接账号就彻底沦陷了。整个过程受害者完全没有感知——没有弹窗、没有跳转、没有需要确认的操作。1.2 三个构成条件拆解浏览器、会话与接口设计CSRF能成立必须有三个条件同时满足缺一个都打不成第一请求必须跨站发出。攻击者构造的页面放在另一个域名下利用浏览器发起对目标站的请求。第二请求需要自动携带身份凭证。传统Cookie Session机制下浏览器对同源请求会自动携带Cookie而且跨站发请求时也一样带——只要目标域名匹配。这是CSRF能成立的物理基础也是现在强调SameSite属性最重要的原因。第三服务端接口必须接受并处理这个请求且操作具有状态改变副作用修改数据、变更权限、触发交易等。CSRF不关心你能不能读到响应因为它不需要读响应它只需要请求被处理。这三个条件里真正能做文章的是第三个。很多老系统的接口设计是能通就行改邮箱用GET、删文章用GET、转账用GET加上没有token校验等于把门敞开了。第二个条件是浏览器的默认行为目前只能靠SameSite从浏览器侧干预。第一个条件是攻击者完全可以控制的。1.3 别和XSS搞混CSRF的核心信任关系入行的时候我花了不少时间才把XSS和CSRF彻底分清楚这里直接用一句话总结XSS利用的是用户对网站的信任CSRF利用的是网站对用户的信任。XSS是攻击者往目标页面里注入了恶意脚本脚本在目标站点的上下文里运行等于拿到了内应。而CSRF里攻击者全程不碰目标页面只是借用浏览器这个中间人的身份去发请求。打个比方XSS是有人混进你家客厅在遥控器上贴了张纸条你每次按遥控器都会执行他编好的动作CSRF是有人隔着窗户用激光笔指挥你家智能音箱不用进你家音箱也认你家的WiFi环境照样能干活。这个区别在防御上很关键XSS可以直接窃取Token、改写页面从而绕过CSRF防御CSRF本身则要求服务端别信任浏览器的自动行为。所以业内常说XSS能杀死CSRF防御——这也就是为什么深度防御必须多层叠加而不能指望单点防护。2. CSRF的攻击形态GET、POST、JSON到接口型变种2.1 经典GET型图片暴击与导航带参GET型CSRF是最古老也最直观的形态。因为浏览器加载外部资源天然会发起GET请求img标签、link预加载、iframe、甚至CSS里的背景图都能触发。构造成本极低img srchttps://example.com/user/logout /别小看这个一像素图片。我在一次授权测试里审计过一个老旧的员工后台退出登录接口是GET且无token。单点看这只是一个强行登出危害不大但它能和另一个开放重定向漏洞组合做成一个受害者很难感知的钓鱼链先被登出再被诱导重定向到仿冒登录页。CSRF的低危通常就是这样被组合放大的。GET型CSRF的检测也最简单Burp里看到敏感操作走了GET且无防护基本可以直接确认。也正因为如此现在稍微有点安全意识的团队都会规定状态改变请求必须用POST。但POST不是免死金牌往下看。2.2 自动提交的表单POST型CSRF的看家本领POST型CSRF是实战中最常见到的。攻击者把表单藏在页面里用户一进页面就自动提交form idcsrf methodPOST actionhttps://example.com/user/changePwd input typehidden namenewpass valueHacked123 / input typehidden nameconfirm valueHacked123 / /form script document.getElementById(csrf).submit(); /script很多新手问跨域POST不是被CORS拦了吗这里必须把概念理顺。浏览器同源策略阻止的是跨域读取响应并不阻止跨域发送请求。普通的application/x-www-form-urlencoded表单提交是简单请求根本不会触发CORS预检浏览器会直接把这个请求发出去响应读不到但请求已经执行了。这也是为什么我们用POST了这句话在CSRF讨论里毫无含金量。只要服务端没有校验请求来源表单自动提交就能绕过。2.3 JSON接口型Content-Type校验与绕过的攻防史现在前后端分离的项目多了很多接口只接收application/json。有人觉得这下安全了吧表单只能提交urlencoded格式啊。这里要说的是JSON接口只是提高了CSRF攻击的构造门槛并非免疫。如果服务端严格校验Content-Type且只认application/json那么跨站发送JSON请求会因为触发CORS预检而被浏览器拦下这是最早大家觉得JSON天然防CSRF的原因。但实际接口经常没这么严格。我看到过不少实现服务端用框架的RequestBody接JSON同时对urlencoded参数的兼容也没有关闭攻击者把JSON字段平铺成POST参数也能被解析。这种情况非常普遍。另一个历史经典绕过是Flash的307重定向可以在跨站上下文中把POST请求及原始Content-Type转发到目标这个在Flash退出历史舞台后基本失效了。现在更常见的思路是检查接口是否接受text/plain甚至application/x-www-form-urlencoded有些网关或框架的容错解析会给你惊喜或者惊吓。所以我的习惯是遇到JSON接口先用不同的Content-Type分别发一次请求观察服务端是否照常处理。如果服务端对非预期Content-Type直接返回415那这个接口的CSRF风险就低很多。2.4 容易被忽略的Login CSRF与OAuth state问题还有两种变体在实战中经常被漏掉。一种是Login CSRF攻击者构造一个登录请求让受害者被登录到攻击者的账号。用户后续在站内产生的浏览记录、订单历史、个人备注全记到了攻击者账号名下长期下来是非常严重的隐私污染。另一种是OAuth授权回调缺少state参数攻击者强制受害者绑定攻击者的第三方账号后续利用账号关联关系做进一步操作。这两种的共同防御思路都是登录和授权流程里加一个不可预测的随机参数并在服务端校验它。3. 授权测试环境下CSRF的发现、验证与利用思路3.1 从Burp流量里挑值得测的接口实战中不可能把所有接口都测一遍CSRF时间上也不允许。我一般按这个优先级筛高敏感且不可逆的操作改绑定手机/邮箱、修改密码、关闭多因子认证、转账、提现。影响面大的操作后台管理员改权限、批量删除、全局配置更新。低频操作这类操作通常没有完善的二次确认开发容易忘加防护。操作越敏感、影响面越大、流程越短越值得优先测。筛选的时候我会重点看Cookie携带情况如果请求Cookie里有sessionid、JSESSIONID这类会话标识而且Header里没有X-CSRF-Token、X-Requested-With之类的自定义头就进入候选名单。3.2 Burp Suite生成CSRF PoC的实操步骤确认候选请求后用Burp生成PoC是最快的验证路径步骤大致是浏览器登录测试账号执行一次目标操作比如修改昵称Burp里找到对应请求。右键选中请求Engagement tools - Generate CSRF PoC。Burp会自动根据请求方法生成带表单或图片标签的HTML。如果是POST把submit()改成页面onload自动执行或者保留一个按钮方便你手动演示。把HTML保存成.html文件放到本地任意目录或临时服务器上用另一个浏览器打开。注意用无痕窗口登录测试账号模拟受害者的状态。打开攻击页面后回到目标站点确认操作是否真的被执行了。这里必须强调一句CSRF验证一定会产生真实的状态改变。我见过有同事在正式环境上拿真实账号测CSRF结果把生产数据改了复盘时非常被动。所有PoC验证必须在授权范围内、用专用测试账号、在测试环境或经过审批的演练环境里做这个是底线。3.3 存在Token时按顺序尝试的绕过手法如果目标接口有Token别急着放弃。CSRF Token的防护效果完全取决于实现质量我通常按下面这套顺序测直接删除Token把Token参数整个去掉如果服务端不校验是否存在而是只比对非空可能会直接放行。Token置空保留参数名传空值。有些框架用了Optional或宽松的反序列化空值也能过。跨会话Token复用自己登录一个账号拿到合法Token放到攻击请求里测试目标账号。如果服务端把Token放在session里但校验逻辑不绑定用户或者Token是全局固定的这招能过。双重提交Cookie缺陷有些实现把Token写进Cookie又要求请求参数里的Token与Cookie里的Token一致攻击者完全可以在自己域名下种一个同名Cookie再配合子域或CRLF注入形成替身Token。换站外链诱导Referer绕过如果服务端只做了Referer关键字匹配比如判断是否包含目标域名可以构造https://example.com.evil.com这样的域名试图绕过。这种弱校验在真实系统里居然还很常见。每测一步都要看响应差异不要想当然。我的经验是光看HTTP状态码不够还要看响应体里有没有错误提示有时200不代表成功302跳转会掩盖真实执行结果。3.4 靶场对比与漏洞定级如果你想系统练CSRF的手感我推荐三个靶场DVWA重点看Impossible级别的完整防护实现能直观理解同步器Token、HTTP Only、SameSite这些概念在代码层面怎么落地。PortSwigger Web Security Academy里面的CSRF实验覆盖Token绕过、Referer校验绕过、Login CSRF等场景跟着做一遍比看十篇文档都管用。pikachu中文靶场适合和同事一起做技术分享时当演示材料。定级方面CSRF在CVSS里通常在中危徘徊但实际危害取决于目标接口性质。改绑手机、关闭MFA这类能直接撬动账号控制权的我认为至少应按高危对待尤其当系统用户基数大、没有其他认证屏障时更要上调。报告里我会把可利用性影响面业务闭环三个维度写清楚避免只给一个冷冰冰的分数。4. 深度防御框架、协议、业务与兜底缺一不可4.1 同步器Token框架内置防御的核心范式先讲最正统的防御范式同步器TokenSynchronizer Token Pattern。核心思想是服务端生成一个不可预测的随机Token存入会话页面渲染时把Token写进表单隐藏域或请求头服务端收到请求后比对请求里的Token与Session中的Token是否一致不一致则拒绝。用代码形容就是# Django风格示意伪代码不做实际实现 from django.views.decorators.csrf import csrf_protect csrf_protect def change_email(request): if request.method POST: if not request.csrf_token request.session[csrf_token]: return HttpResponseForbidden(CSRF check failed) # 正常逻辑 ...现代主流框架基本都内置了这个机制Spring Security的CsrfTokenRepository、Django的CSRF中间件、Laravel的VerifyCsrfToken、Express生态的csurf。我强烈建议直接用框架默认实现别自己造轮子。自己写Token校验最常见的坑就是校验了但校验不彻底比如只在某几个接口加了校验而漏掉了新增接口或者Token生成后没有足够随机性。4.2 SameSite Cookie浏览器侧最有效的一道闸CTF和实战里经常听到SameSite一开CSRF少一半这句话虽然不够严谨但方向是对的。Chrome 80之后未显式指定SameSite的Cookie默认按Lax处理这直接拦截掉了绝大多数跨站POST请求。三种模式的区别我整理成表格属性值跨站GET顶级导航跨站GETimg/iframe等子资源跨站POST适用场景Strict不携带Cookie不携带Cookie不携带Cookie安全要求最高的核心站点Lax携带Cookie不携带Cookie不携带Cookie绝大多数站点默认选择None Secure携带Cookie携带Cookie携带Cookie必须跨站携带Cookie的场景如单点登录需要注意Lax虽然拦了子资源加载和POST但顶层导航的GET仍然会携带Cookie。如果系统里还有GET型状态改变接口Lax也救不了你。另外同站不等于同源www.example.com和login.example.com在SameSite判定里是同一站都是example.com的子域互相发请求不受影响。这个细节在评估多域名系统时特别容易踩。4.3 业务二次校验防御绕过后的最后底线框架层和协议层都可能被绕过所以真正高价值的操作必须在业务层加一道人工确认。我见过的比较可靠的做法有改绑手机/邮箱强制输入当前密码或发送短信验证码到原手机号。修改密码要求先完成一次账号内的高强度认证再允许进入改密流程。关闭双因子认证必须有二次确认交互比如弹窗、验证码、短时令牌。提现/转账U盾、动态令牌、手机验证码三选一。这类校验的优势在于就算某个CSRF绕过手法把框架防御全打穿了攻击者也无法完成业务闭环——他拿不到用户的密码收不到短信调不起二次确认。这也是为什么我们说深度防御因为每一层都有能力单独兜住漏洞。4.4 Origin/Referer校验与接口约定如果不想全面引入Token机制比如纯接口服务至少应该做来源校验。我最推荐校验Origin请求头因为它由浏览器保证包含精确的协议域名端口而且不会被隐私策略裁剪掉。Referer作为辅助手段也有价值但有几个坑部分浏览器/隐私扩展会不发送Referer服务端如果直接拒绝无Referer请求会误伤正常用户。Referer里可能包含敏感URL路径不利于隐私。弱校验只判断是否包含目标域名很容易被example.com.evil.com绕过。另一个被低估的接口约定是对自定义Header的利用。跨站简单请求无法自定义Header否则会触发预检所以服务端要求请求头必须带X-Requested-With: XMLHttpRequest或业务自定义的X-Client-Type能挡住一大片表单自动提交。但这个方法有一个前提CORS配置必须是严格白名单不能让Access-Control-Allow-Origin: *和Access-Control-Allow-Headers: *同时出现否则预检一过自定义头就形同虚设了。4.5 CORS防线与CSRF防御不可混为一谈这个坑我见得太多了单独拿出来说。CORS解决的问题是跨域读取响应CSRF防御解决的问题是跨站发送请求要不要被接受。这两个概念经常被混在一起导致两个方向都做错。比如有人把Access-Control-Allow-Origin配成动态反射并让它跟着Origin头回显本意是方便前端跨域调用结果等于把CSRF防御的辅助手段也废了。又比如有人以为CORS默认不允许跨域所以CSRF也没事——实际上普通表单请求是简单请求根本不触发CORS机制该来的CSRF照样来。正确的理解是CORS应该只开放给可信来源且永远不要通配符携带凭据同时出现。CSRF防御要靠前面讲的Token、SameSite、来源校验来解决。做好CORS是必要的但它绝不能替代CSRF防御。5. 防了等于没防几个典型翻车现场与检查清单5.1 看起来有防护实际能被绕过的五种配置这些年审核过的CSRF防护代码里反复出现五种表面安全的实现我列出来给大家提个醒只在登录接口做CSRF防护忽略了登录后最敏感的那批操作。攻击者需要的是已登录用户的接口不是登录接口本身。Referer校验写成包含域名即通过结果被https://example.com.attacker.com绕过或者因为浏览器不发Referer而全部拒绝造成大量真实用户无法操作。双重提交Cookie把Token放在Cookie和参数里做一致性比对但忘了校验两个Token是否都来自同一Cookie攻击者在自己控制域下种Cookie就能伪造。Token没有与Session绑定一个Token全局通用属于有防护和没防护一个样。SPA项目把业务Token放进Cookie图拦截器自动携带方便却不设SameSite属性等于把CSRF攻击面重新打开。前后端分离项目正确的做法是用Authorization: Bearer头传递Token天然避开CSRF这一类问题。5.2 一份可直接对照的自查清单我把这些年检查和修复CSRF问题的经验整理成一份清单开发和测试都能直接对照着过检查项建议做法不推荐状态改变请求方法必须POST/PUT/DELETE并在服务端限制方法用GET承载敏感操作会话Cookie属性SameSiteLax或Strict Secure HttpOnlySameSite未设置旧浏览器Token机制同步器Token/框架内置实现与Session绑定全局固定Token双重提交Cookie可用但必须确认Cookie与参数一致性校验逻辑只比对参数与Cookie字面值来源校验校验OriginReferer作辅助只判断Referer是否含域名JSON接口严格校验Content-Type拒绝非预期格式同时接受JSON与urlencoded参数自定义请求头服务端要求X-Requested-With等自定义头没有要求、CORS配置通配高敏感操作增加验证码/二次密码/短信校验仅依赖框架层防御前端存储SPA用Authorization头传Token业务Token放Cookie且无SameSite5.3 测试与报告过程中的几点个人建议最后分享几条从实际项目里攒出来的经验。第一CSRF的测试一定在授权范围内进行用专门的测试账号不要在正式环境对真实用户操作做验证。第二报告里除了完整复现路径和PoC一定要给出针对当前项目的修复建议最好直接写到代码层面是加中间件、改Cookie属性、还是在业务层加二次校验都要说清楚。第三别把CSRF单独当一个漏洞看待看看它能不能与其他问题组合开放重定向、CORS错误配置、子域XSS都会让CSRF的实际危害成倍增长。我个人做安全测试时还有一个习惯把自己想象成最懒的攻击者——不写脚本、不搞插件就发一个HTML文件给目标浏览器看它能不能被打穿。CSRF漏洞漏洞往往就藏在开发觉得这不可能被利用的自信里。你把这条思路走通了这个漏洞基本就跑不掉了。