1. 项目概述:从一次“被转账”说起
几年前,我在一个内部安全测试项目中,模拟了一次针对某电商后台的“无害”攻击。我让一位登录了后台的管理员同事,点击了我发在内部聊天群里的一个“搞笑图片”链接。几秒钟后,系统日志显示,他的账户在不知情的情况下,创建了一个新的、拥有高级权限的测试账号。整个过程他毫无察觉,页面只是快速闪了一下,看起来就像图片加载失败。这次经历让我对CSRF(Cross-Site Request Forgery,跨站请求伪造)这个看似“古老”的漏洞,有了刻骨铭心的认识。它不像SQL注入或XSS那样直接窃取数据,而是像一个“提线木偶大师”,在用户毫无防备时,操纵他们的浏览器向信任的网站发起恶意请求。
CSRF漏洞的原理并不复杂,但危害极大,且极易被开发者忽视。它利用了Web应用对用户浏览器的完全信任机制:浏览器会自动携带用户的登录凭证(如Cookie、Session ID)去访问已认证的站点。攻击者要做的,就是构造一个恶意请求,并诱骗已登录的目标用户去触发它。这个请求可以是转账、改密、发帖、添加管理员,任何用户权限内的操作都可能成为攻击目标。随着当前应用架构日益复杂,前端与后端分离(如React、Vue + RESTful API)、各种第三方库和组件的引入,CSRF的防御面临着新的挑战,也出现了新的攻击面。理解其原理、亲手复现案例、并掌握有效的修复策略,是每一位Web应用开发者、测试人员和安全工程师的必修课。无论你是刚入门的安全爱好者,还是负责线上业务稳定的架构师,这篇文章都将带你从攻击者的视角理解CSRF,再从防御者的角度筑牢防线。
2. CSRF漏洞核心原理深度拆解
要防御CSRF,必须首先透彻理解它的攻击链条。这不仅仅是一个技术点,更是一种对Web交互信任模型的挑战。
2.1 信任与背叛:浏览器的“自动提交”机制
Web的基石之一是“状态保持”,通常通过Session机制实现。用户登录后,服务器会创建一个Session,并将对应的Session ID通过Set-Cookie头传递给浏览器。此后,浏览器向该域名下的任何请求,都会 自动 在HTTP头中携带这个Cookie。这是浏览器的标准行为,旨在提供无缝的用户体验。
CSRF攻击正是滥用了这份“自动”的信任。攻击者并不需要窃取你的Cookie(那是XSS常干的事),他只需要你知道,你的浏览器会忠实地带着Cookie去访问某个网站。假设银行转账的接口是
POST /transfer
,需要参数
to_account
和
amount
。一个已登录的用户访问这个接口时,请求头里自然包含了他的身份Cookie。
攻击者的思路是:“我不需要知道你的Cookie是什么,我只需要让你,在登录银行网站的状态下,去访问一个我精心准备的页面。这个页面里藏着一个向
/transfer
发起的请求。” 由于请求是从用户的浏览器发出的,Cookie会被自动带上,服务器就会认为这是用户本人的合法操作。
2.2 攻击的必要条件与经典场景
一次成功的CSRF攻击通常依赖以下几个条件同时成立:
- 关键操作未受CSRF保护 :应用在执行业务逻辑(如修改数据、触发交易)时,仅依赖Cookie/Session进行身份验证,没有校验请求的“意图”是否真正来源于用户自愿发起的页面。
- 用户已登录目标站点并保持会话 :用户的浏览器中持有有效的、未过期的认证凭证。
- 用户被诱骗访问恶意页面 :攻击者通过社交工程(如钓鱼邮件、论坛帖子、聊天消息)诱导用户点击一个链接或访问一个网站。这个页面可能看起来人畜无害,甚至就是一个空白页。
攻击场景五花八门:
- 社交网络 :诱使用户在不知情下发布状态、关注某人、点赞某条内容。
- 电商平台 :修改收货地址至攻击者地址,或用用户账户发起购买。
- 网银系统 :发起转账交易。
- 后台管理系统 :添加后台管理员、修改系统配置。
- 邮箱系统 :配置邮件转发规则,将用户所有邮件秘密转发给攻击者。
注意 :这里需要严格区分CSRF和XSS。XSS是在目标网站中注入并执行恶意脚本,直接窃取用户在该站点的数据(如Cookie)。而CSRF是借用用户在其他站点的登录状态,从“外部”发起伪造请求。两者有时会结合使用(如用XSS获取Token再构造CSRF),但原理截然不同。
2.3 请求伪造的多种载体与手法
攻击者如何让用户的浏览器发出那个恶意请求?手法非常灵活:
-
自动提交的HTML表单 : 这是最经典的方式。在恶意页面中嵌入一个隐藏的
<form>,表单的action指向目标网站的敏感接口,method设为POST,并预先填好参数。通过一段JavaScript脚本在页面加载时自动提交表单。<body onload="document.forms[0].submit()"> <form action="https://victim-bank.com/transfer" method="POST"> <input type="hidden" name="to_account" value="ATTACKER_ACCOUNT" /> <input type="hidden" name="amount" value="10000" /> <!-- 如果需要,甚至可以伪造其他参数 --> </form> </body>用户访问这个页面,表单立即自动提交,请求发出。
-
IMG标签的GET请求 : 如果敏感操作不幸地使用了GET方法(这本身是错误的设计),攻击将变得更加简单。只需要在恶意页面中插入一个图片标签,其
src属性就是携带参数的请求URL。<img src="https://victim-bank.com/transfer?to_account=ATTACKER&amount=10000" width="0" height="0" />浏览器会尝试加载这个“图片”,从而发起一个GET请求。即使图片加载失败,请求也已经发出。
-
AJAX/Fetch请求(受同源策略限制,但仍有方法) : 现代前端应用大量使用AJAX。虽然浏览器的同源策略默认会阻止跨域的AJAX请求读取响应,但 请求本身可以被发送出去 !对于很多“只做事不返回敏感数据”的接口(如修改操作),这已经足够了。攻击者页面中的JavaScript可以直接用Fetch API发起一个POST请求,用户的Cookie依然会被带上。
fetch('https://victim-bank.com/api/transfer', { method: 'POST', credentials: 'include', // 关键!指示浏览器携带Cookie headers: {'Content-Type': 'application/json'}, body: JSON.stringify({to: 'ATTACKER', amount: 10000}) });如果服务器没有检查
Origin或Referer头,这个请求就会被成功执行。 -
链接点击 : 将恶意请求伪装成一个普通链接,诱使用户点击。
<a href="https://victim-bank.com/transfer?to_account=ATTACKER&amount=10000"> 点击领取您的年度大奖! </a>
3. 实战复现:在DVWA中亲手触发CSRF
理解了原理,最好的巩固方式就是亲手复现。我们使用Damn Vulnerable Web Application (DVWA) 这个经典的漏洞练习平台,它内置了从Low到High不同安全等级的CSRF漏洞场景。
3.1 环境搭建与目标设定
首先,你需要一个本地DVWA环境。可以通过Docker快速部署:
docker run --rm -it -p 80:80 vulnerables/web-dvwa
访问
http://localhost
,按照提示完成安装(数据库设置等),默认登录账号/密码为
admin
/
password
。
我们的攻击目标是DVWA的“CSRF”模块,具体是“修改密码”功能。在真实场景中,这等价于攻击一个“修改用户资料”或“重置密码”的接口。攻击者的目标是:在用户不知情的情况下,将其密码修改为攻击者设定的值。
3.2 Low安全级别:毫无防护的“裸奔”状态
将DVWA安全级别设置为 Low 。进入CSRF模块,你会看到一个简单的修改密码表单,需要输入新密码和确认密码。
1. 分析请求: 提交一次修改,用浏览器开发者工具(F12)的“网络(Network)”标签捕获请求。你会发现这是一个GET请求,URL形如:
http://localhost/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#
所有参数都明文暴露在URL中。服务器仅通过Session判断用户身份。
2. 构造攻击页面:
攻击简单到令人发指。创建一个名为
attack_low.html
的本地文件,内容如下:
<!DOCTYPE html>
<html>
<body>
<h1>你中奖了!</h1>
<!-- 利用IMG标签发起GET请求 -->
<img src="http://localhost/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change#" />
<p>请稍候,正在为您加载奖品...</p>
</body>
</html>
这个页面会尝试加载一个“图片”,其src就是修改密码的请求URL,将密码改为“hacked”。
3. 实施攻击:
- 用浏览器登录DVWA(保持Session)。
-
在同一个浏览器的新标签页中,打开
file:///path/to/attack_low.html。 - 观察DVWA页面,你会发现密码已经被修改了。攻击页面可能显示图片加载错误,但这无关紧要,请求已经完成。
实操心得 :在Low级别下,CSRF防御为0。这警示我们, 绝对不要使用GET方法进行状态变更操作 。GET请求应设计为幂等的、仅用于获取资源。任何修改、删除、创建操作都必须使用POST、PUT、DELETE等方法。这是防御CSRF的第一道,也是最重要的编码规范。
3.3 Medium安全级别:脆弱的Referer检查
将安全级别切换到
Medium
。再次尝试提交修改密码,你会发现请求变成了POST方式。查看后端源码(或通过抓包分析),会发现Medium级别增加了对
HTTP Referer
头的检查:它要求请求的Referer必须包含服务器的主机名(如
localhost
)。
1. 绕过思路:
Referer
头表示请求来源于哪个页面。如果攻击页面位于
http://attacker.com/evil.html
,那么Referer就是attacker.com,会被服务器拒绝。但是,Referer可以被伪造吗?在浏览器环境下,普通网页无法直接修改Referer头。但我们可以尝试让它“缺失”或“变得合法”。
2. 利用同源或空Referer: 有两种常见绕过方式:
- 从HTTPS跳到HTTP :一些浏览器在从HTTPS页面发起HTTP请求时,出于安全考虑,可能会省略Referer头。我们可以搭建一个简单的HTTPS页面来发起攻击(需要自签名证书)。
-
利用数据协议或本地页面
:如果我们可以让用户访问一个
data:text/html,...或file:///协议下的页面,这些页面发起的跨站请求,其Referer可能是null或空。在某些检查不严谨的实现中(比如只检查Referer是否存在,而不严格校验其内容),可能被绕过。
3. 构造攻击页面(示例):
假设服务器检查逻辑是“Referer必须包含localhost”,我们可以尝试让Referer为空。一种方法是使用一个重定向。创建
attack_medium.php
放在攻击者服务器上:
<?php
// 立即重定向到恶意请求URL,不设置Referer(或Referer是attacker.com,但重定向后可能变化)
header("Location: http://localhost/vulnerabilities/csrf/?password_new=hacked2&password_conf=hacked2&Change=Change#");
?>
然后诱使用户访问
http://attacker.com/attack_medium.php
。用户点击后,浏览器会直接跳转到目标URL。此时,Referer可能是attacker.com,也可能是空,取决于浏览器和跳转方式。如果DVWA的Medium检查不够严格,攻击可能成功。
注意事项 :依赖Referer防御CSRF并不可靠。首先,Referer头可能因用户隐私设置(如浏览器“不发送Referer”选项)或网络中间件(如某些代理、防火墙)而被剥离。其次,检查逻辑如果写得不严谨(例如用字符串包含
indexOf(‘localhost’)而不是解析主机名并严格比对),很容易被绕过。因此, Referer检查只能作为辅助手段,绝不能作为唯一的防御措施 。
3.4 High安全级别:引入Anti-CSRF Token
将安全级别切换到
High
。此时修改密码表单中,会多出一个隐藏的输入框
user_token
,其值是一个随机的、与当前Session绑定的令牌。
1. Token机制原理: 服务器在渲染表单时,生成一个随机数(Token),将其存储在服务器端的Session中,同时输出到表单的隐藏域。当用户提交表单时,必须将这个Token一并提交。服务器收到请求后,会比对提交的Token和Session中存储的是否一致。因为攻击者无法提前知晓或预测这个Token值(它随Session和每次请求变化),所以他构造的恶意请求中无法包含有效的Token,请求会被拒绝。
2. 为何High级别仍可能被攻破? 在DVWA High级别中,Token是随着表单页面一起下发的。如果攻击者能先通过某种方式(例如,结合一个XSS漏洞)获取到包含有效Token的表单页面,他就能从中提取Token,并构造出合法的请求。这属于“混合漏洞”攻击。单纯的CSRF在完善的Token机制前是无效的。
3. 安全编程实践:
- Token需满足 :随机性高、与用户Session强绑定、一次性使用(或短时间有效)。
-
Token放置位置
:不仅要在表单中,对于重要的API接口,也应要求通过自定义HTTP头(如
X-CSRF-Token)或请求体携带Token。 -
Token的存储与提交
:前端通常将Token放在
<meta>标签或全局变量中,由JavaScript在发起AJAX请求时自动添加到请求头。这催生了像axios这样的库内置了CSRF Token支持。
4. 全面修复策略:从开发到部署的防御体系
防御CSRF需要一个多层次、纵深防御的策略,不能只依赖单一方法。
4.1 黄金标准:同步令牌模式
这是最主流、最有效的防御方案,如前文所述。
-
实现要点
:
- 服务器为每个用户会话生成一个强随机数的CSRF Token。
- 在渲染任何可能执行状态变更操作的页面(表单)时,将此Token嵌入。对于单页面应用(SPA),可以在用户登录后,通过一个API将Token返回给前端,前端将其存储。
-
前端在发起非幂等请求(POST, PUT, DELETE, PATCH)时,必须携带此Token。方式可以是:
-
表单隐藏域
:
<input type="hidden" name="csrf_token" value="..."> -
自定义HTTP头
:如
X-CSRF-Token: ...。这种方式更安全,因为跨域请求默认无法发送自定义头(需要CORS配合)。
-
表单隐藏域
:
- 服务器端在处理请求前,校验Token的有效性和匹配性。
-
框架集成
:几乎所有主流Web框架都内置了CSRF防护中间件,如Django的
CsrfViewMiddleware, Spring Security的CsrfFilter, Laravel的VerifyCsrfToken中间件。 强烈建议直接使用框架提供的方案 ,而不是自己从头实现,以避免因理解偏差引入逻辑漏洞。
4.2 双重Cookie验证
这是一种在API场景下常用的简化方案,尤其适合前后端分离且同域部署的项目。
-
原理
:用户登录后,后端不仅在Cookie中设置Session标识,还会生成一个CSRF Token,将其放在另一个Cookie中(如
X-CSRF-Token)。前端JavaScript代码可以读取这个Cookie(因为同源),在发起请求时,将其值放入一个自定义HTTP头(如X-CSRF-Token)中。服务器同时验证Cookie中的Token和请求头中的Token是否一致。 - 优点 :实现相对简单,无需为每个表单单独注入Token。
-
缺点
:
- 依赖Cookie可读 :需要确保CSRF Token的Cookie路径和域设置正确,且前端能访问到。
-
子域风险
:如果应用有多个子域(如
a.example.com,b.example.com),且Cookie的域设置为.example.com,那么一个子域上的恶意页面可以读取和操作父域的Cookie,从而绕过防护。此时需要严格隔离或使用更严格的Token绑定策略。 - 如果网站存在XSS漏洞,攻击者可以轻易读取Cookie中的Token,从而使此防护失效。因此, 双重Cookie验证不能替代XSS的防护 。
4.3 检查自定义HTTP头
这是一个非常轻量且有效的辅助手段,常与Token结合使用。
-
原理
:对于异步请求(AJAX/Fetch),要求前端必须设置一个自定义的HTTP头,如
X-Requested-With: XMLHttpRequest。服务器端检查请求中是否存在这个头。 -
为何有效
:根据浏览器的同源策略和CORS规范,默认情况下,跨域请求无法由脚本添加自定义HTTP头。攻击者从
evil.com发起的伪造请求,无法添加X-Requested-With头。而你的合法前端代码(同源)可以轻松添加。 -
局限性
:这只对由JavaScript发起的请求有效。对于传统的HTML表单提交或由
<img>,<script>标签发起的请求无效。因此,它通常作为API层的一道额外防线,不能单独使用。
4.4 同源检测与Referer策略
如前所述,检查请求的
Origin
或
Referer
头是一个补充手段。
-
Origin vs Referer
:
Origin头只包含协议、主机和端口,不包含路径,更简洁,且对于隐私敏感请求(如POST),浏览器一定会发送。Referer包含完整路径,但可能被浏览器策略或扩展程序屏蔽。 -
推荐检查Origin
:对于接收JSON或XML的API端点,严格检查
Origin头是否在白名单内(通常是你的应用前端域名)。这可以有效阻止来自未知源域的跨站请求。 -
配置CORS
:正确配置跨域资源共享(CORS)策略。对于敏感操作,不要使用
Access-Control-Allow-Origin: *。明确指定允许的来源域名。CORS本身不是CSRF防御机制,但严格的CORS策略可以阻止某些类型的跨域恶意请求。
4.5 关键操作增加二次确认
在业务逻辑层增加软性防护。对于特别敏感的操作(如转账、修改密码、删除账户),要求用户进行二次确认。例如:
- 输入登录密码再次验证。
- 使用手机短信验证码或邮箱验证码。
- 图形滑块或点选验证。
这虽然不是技术上的根本解决方案(因为一个设计良好的CSRF攻击链甚至可以模拟点击“确认”按钮),但能极大提高攻击门槛,并给用户一个察觉异常的机会。
5. 现代架构下的新挑战与进阶防护
在单页应用(SPA)、微服务、第三方组件集成的现代开发模式下,CSRF防御需要考虑更多细节。
5.1 SPA与Token管理
在Vue、React等SPA中,页面生命周期内不会整体刷新,Token如何管理?
- 登录后获取 :用户登录成功后,后端在响应体中返回一个CSRF Token(不要只放在Cookie里)。前端将其存储在内存(如Vuex、Redux)或Web Storage中。
-
请求拦截器
:使用Axios等HTTP客户端的请求拦截器,自动为每一个非GET请求添加Token到自定义头。
// axios 示例 import axios from 'axios'; const csrfToken = getTokenFromStore(); // 从状态管理获取token axios.interceptors.request.use(config => { if (config.method !== 'get' && config.method !== 'GET') { config.headers['X-CSRF-Token'] = csrfToken; } return config; }); - Token刷新 :可以考虑为Token设置较短的有效期,并提供刷新接口。或者,采用“每个会话单Token”策略,直到用户注销或会话过期。
5.2 第三方组件与库的安全审计
项目中引入的第三方JavaScript库、npm包,都可能成为CSRF攻击的入口。一个被篡改或本身就存在恶意代码的库,可以在你的网站上下文中发起任意请求。
-
依赖管理
:定期使用
npm audit、yarn audit或专业的SCA(软件成分分析)工具扫描依赖漏洞。 -
内容安全策略(CSP)
:实施严格的CSP,限制脚本只能从可信源加载,禁止内联脚本执行 (
‘unsafe-inline’)。这可以极大程度上防止恶意脚本的注入和执行,间接防御了需要脚本配合的复杂CSRF攻击。 -
子资源完整性(SRI)
:对于直接从CDN引用的第三方库,使用SRI哈希来确保文件内容未被篡改。
<script src="https://cdn.example.com/library.js" integrity="sha384-...sha512-hash..." crossorigin="anonymous"></script>
5.3 API网关与统一防护
在微服务架构中,每个服务单独实现CSRF防护容易遗漏。可以在API网关层实施统一的防护策略:
- 在网关处校验所有进入的请求,检查必要的CSRF Token或自定义头。
- 对路由进行区分,只对需要认证且非幂等的路由进行校验。
- 这样可以将安全逻辑集中,降低业务服务的复杂度。
5.4 常见问题与排查技巧实录
在实际开发和渗透测试中,会遇到各种关于CSRF防护的疑难杂症。下面是一个快速排查表:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 表单提交后,CSRF Token校验总是失败。 |
1. Token未正确绑定Session。
2. 前端提交的Token字段名与后端预期不符。 3. 使用了多台服务器,Session未共享。 |
1. 检查服务器Session存储,确认生成和校验的是同一个Session ID下的Token。
2. 对比前端提交的请求体/头和后端代码中读取的字段名。 3. 如果是分布式部署,确保使用集中式的Session存储(如Redis)。 |
| AJAX请求能成功,但传统表单提交失败。 |
1. Token只放在了JavaScript可访问的地方(如meta标签),未注入到HTML表单中。
2. 双重Cookie验证模式下,表单提交无法携带自定义头。 |
1. 确保渲染服务器端模板时,将Token写入表单的隐藏域。
2. 对于传统表单,应使用隐藏域方式提交Token。对于AJAX,可以使用自定义头。后端需同时支持两种方式。 |
| 在SPA中,打开多个标签页操作,Token失效。 |
1. Token设计为一次性使用,在A页使用后,B页的Token就失效了。
2. 多个标签页共享同一个Session,但Token生成逻辑有误。 |
1. 评估是否真的需要一次性Token。对于大多数场景,一个会话期内使用同一个Token是安全的,只需确保Token足够随机且绑定Session。
2. 确保Token生成算法是线程/进程安全的。 |
| 开启了CORS,但CSRF攻击似乎仍然可能。 |
错误配置了
Access-Control-Allow-Origin: *
并且允许携带凭证 (
Access-Control-Allow-Credentials: true
)。
|
绝对禁止
在允许携带凭证的情况下使用通配符(
*
)来源。必须明确指定允许的来源白名单。检查CORS中间件配置。
|
| 使用了框架的CSRF防护,但自定义的API接口依然被绕过。 | 框架的CSRF中间件可能默认只保护某些HTTP方法(如POST)或某些URL模式。自定义的API路由未被包含。 | 检查框架CSRF中间件的配置,确保其保护了所有需要认证的非GET端点。可能需要手动将自定义路由添加到保护列表中。 |
| 测试时,从Postman发起的请求可以绕过CSRF检查。 | Postman等工具不会自动携带浏览器的Cookie,也不会受同源策略限制。它模拟的是“客户端直接请求”,而非“跨站请求”。 | 切记 :CSRF防护是针对浏览器环境的。用工具测试时,需要手动在请求中添加正确的CSRF Token和Cookie,以验证Token机制本身是否工作正常。不能因为Postman能请求就认为有漏洞。 |
最后再分享一个我踩过的坑
:在一次项目迁移中,我们将部分静态资源放到了独立的CDN域名下。之后发现CSRF Token校验时通时断。排查后发现,是因为主页面从CDN加载的一个通用JS库,也尝试读取
meta
标签中的Token去发起请求,但由于CDN域名不同,它读取不到主域名下的
meta
标签。这提醒我们,在设置Token的存储和访问方式时,必须充分考虑资源的跨域情况。最终我们改为由后端API在登录响应中返回Token,前端JS库通过全局变量或状态管理来获取,彻底解耦了对DOM的依赖。

426

被折叠的 条评论
为什么被折叠?



