news 2026/10/17 5:07:56

CSRF攻击原理与防御实战:从伪造请求到Token防护

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CSRF攻击原理与防御实战:从伪造请求到Token防护

1. 这个“看不见的点击”到底在偷什么?

很多人第一次听说CSRF(Cross-Site Request Forgery,跨站请求伪造)时,第一反应是:“不就是点个链接吗?我天天点,怎么就成攻击了?”——这恰恰是它最危险的地方。它不依赖用户主动输入密码、不诱导你下载木马、甚至不需要你意识到自己点了什么。它利用的是浏览器一个根深蒂固的信任机制:只要请求是从你的浏览器发出去的,服务器就默认是你本人操作的。

举个生活化的例子:你去银行柜台办业务,柜员不会每次转账都重新核验你的身份证原件,而是看你是不是本人到场、是否持有有效证件。CSRF就像有人偷偷复制了你的身份证复印件,再找一个和你穿同样衣服、戴同款眼镜的替身,站在你常去的那家银行柜台前,用你的名义办了一笔转账。银行(服务器)看到“证件”(Cookie)是真的、“人”(请求来源)也像模像样,就放行了。而真正的你,可能正躺在沙发上刷短视频,对这笔转账一无所知。

这个标题里写的“web渗透测试——14、CSRF”,不是教你怎么去黑别人,而是告诉你:作为开发者、安全工程师或运维人员,你必须亲手复现一次这种“无声的劫持”,才能真正理解它的触发链条有多脆弱、防御边界在哪里、以及为什么一个看似无关紧要的GET请求,可能比十个复杂的SQL注入更致命。它不考验你多会写0day,而是拷问你对Web基础协议的理解深度——HTTP无状态、Cookie自动携带、同源策略的盲区、浏览器默认行为……这些被我们每天调用却很少深究的底层逻辑,正是CSRF扎根的土壤。

关键词里虽然空着,但整个场景的核心词其实非常聚焦:伪造(Forgery)、请求(Request)、会话(Session)、Cookie、Referer、Origin、Token、SameSite。它们不是孤立的术语,而是一条完整的攻击-防御链上的齿轮。比如,你肯定知道Cookie是用来维持登录态的,但你是否清楚,当一个恶意网站页面里嵌入<img src="https://bank.com/transfer?to=attacker&amount=10000">时,浏览器会毫不犹豫地带上你在bank.com的登录Cookie去请求这张“图片”?而bank.com的转账接口如果只校验Cookie有效就执行操作,这笔钱就真的转走了——整个过程,用户连鼠标都没动一下。

所以这篇内容,不是给你讲概念定义,而是带你从零搭建一个可交互的CSRF靶场,亲手触发一次真实攻击,再一层层剥开防御方案的实现细节。你会看到,防御CSRF不是加一行代码就能搞定的事,它牵扯到前端渲染逻辑、后端中间件设计、甚至CDN缓存策略。很多团队在上线前做安全扫描,报告里标红“高危:CSRF漏洞”,开发同学第一反应是“加个token不就完了?”,结果上线后发现所有AJAX请求全挂了,因为token没同步刷新;或者移动端App调用API时突然403,因为App没处理SameSite=Lax的Cookie丢弃逻辑。这些坑,只有亲手踩过,才知道哪块石头底下藏着雷。

2. 从零构建可复现的CSRF攻击链:靶场搭建与攻击验证

要真正理解CSRF,光看理论等于纸上谈兵。我们必须在一个可控、干净、能清晰观测每一步的环境中,把攻击链条完整跑通。这里不推荐直接用DVWA或WebGoat这类集成靶场,因为它们的代码封装太深,你很难看清请求头里Referer字段是怎么被篡改的、Token是如何在表单和AJAX中传递的。我们要自己搭一个极简但五脏俱全的靶场,核心就三个文件:一个登录页、一个转账功能页、一个恶意诱导页。全部用原生PHP+HTML实现,不依赖任何框架,确保每个字节都暴露在你眼皮底下。

2.1 靶场环境准备:三步到位,拒绝黑盒

首先明确环境要求:一台能跑PHP的本地机器(Mac/Linux用内置Apache+PHP,Windows用XAMPP/WAMP均可),不需要数据库,所有状态用Session管理。关键点在于禁用所有外部干扰:关闭浏览器插件(尤其是广告拦截和隐私保护类)、清空当前域名下的所有Cookie、确保靶场域名是localhost或127.0.0.1(避免Chrome对localhost的特殊SameSite策略)。我试过用http://test.local,结果Chrome 115+版本默认给所有非HTTPS的.local域名加SameSite=Lax,导致攻击根本触发不了——这种细节,文档里不会写,但实操中会让你卡一整天。

具体搭建步骤:

  1. 创建项目目录:在Web根目录下新建csrf-lab文件夹,里面放三个文件:login.php、transfer.php、malicious.html。

  2. 实现登录逻辑(login.php):

<?php session_start(); if ($_POST['username'] === 'admin' && $_POST['password'] === 'pass123') { $_SESSION['logged_in'] = true; $_SESSION['user_id'] = 'admin_123'; header('Location: transfer.php'); exit; } ?> <!DOCTYPE html> <html> <head><title>Login</title></head> <body> <h2>CSRF Lab Login</h2> <form method="POST"> <input type="text" name="username" placeholder="Username" required><br> <input type="password" name="password" placeholder="Password" required><br> <button type="submit">Login</button> </form> </body> </html>

注意:这里密码明文存储纯为演示,实际绝不可行。Session启动后,$_SESSION['logged_in']就是后续所有敏感操作的准入凭证。

  1. 实现核心转账功能(transfer.php):
<?php session_start(); if (!isset($_SESSION['logged_in']) || !$_SESSION['logged_in']) { header('Location: login.php'); exit; } // 模拟转账逻辑 if ($_GET['action'] === 'transfer' && isset($_GET['to']) && isset($_GET['amount'])) { $to = htmlspecialchars($_GET['to']); $amount = (int)$_GET['amount']; // 真实场景这里会查数据库、扣余额、记日志 echo "<p style='color:green'>✅ Transfer successful! $amount to $to</p>"; // 关键:记录本次操作,用于后续验证攻击是否生效 file_put_contents('log.txt', date('Y-m-d H:i:s') . " - Transferred $amount to $to\n", FILE_APPEND); } ?> <!DOCTYPE html> <html> <head><title>Transfer Page</title></head> <body> <h2>Welcome, Admin!</h2> <p>Your balance: <strong>$99,999</strong></p> <h3>Make a Transfer</h3> <!-- 注意:这是典型的CSRF易感点——GET请求执行状态变更 --> <a href="transfer.php?action=transfer&to=attacker&amount=1000">Click to send $1000 to attacker</a><br><br> <!-- 同时提供一个POST表单,对比两种方式的防御差异 --> <form method="POST" action="transfer.php"> <input type="hidden" name="action" value="transfer"> <input type="text" name="to" placeholder="Recipient" required> <input type="number" name="amount" placeholder="Amount" required> <button type="submit">Send via POST</button> </form> </body> </html>

这里故意暴露两个入口:一个是GET链接(极度危险),一个是POST表单(相对可控)。你会发现,攻击者最喜欢前者,因为构造成本为零。

2.2 发起真实攻击:三分钟复现“静默转账”

现在打开浏览器,访问http://localhost/csrf-lab/login.php,用admin/pass123登录。成功后跳转到transfer.php,此时浏览器已持有该域名下的Session Cookie。接下来,不要关闭这个标签页——这是攻击成立的前提:用户必须处于已登录状态。

新建一个空白HTML文件malicious.html,内容如下:

<!DOCTYPE html> <html> <head><title>Free iPhone Giveaway!</title></head> <body> <h1>🎉 Congratulations! You won an iPhone 15!</h1> <p>Click below to claim your prize:</p> <!-- 关键:隐藏的iframe,用户看不到,但浏览器会加载 --> <iframe src="http://localhost/csrf-lab/transfer.php?action=transfer&to=hacker_wallet&amount=5000" width="0" height="0" style="display:none;"></iframe> <!-- 或者用img标签,效果一样 --> <!-- <img src="http://localhost/csrf-lab/transfer.php?action=transfer&to=hacker_wallet&amount=5000" width="0" height="0" style="display:none;"> --> <button onclick="alert('Prize claimed! Check your email.')">CLAIM NOW</button> </body> </html>

把这个文件用浏览器直接打开(file:///path/to/malicious.html),或者放到另一个Web服务下(如http://localhost/malicious.html)。点击“CLAIM NOW”按钮(或什么都不点,因为iframe已自动加载),然后立刻切回transfer.php标签页——你会看到页面上多出一行绿色提示:“✅ Transfer successful! 5000 to hacker_wallet”。同时,检查项目目录下的log.txt,里面新增了一条时间戳记录。

提示:如果没成功,请立即检查三件事:(1)两个页面是否都在同一个浏览器窗口/标签页中(不同窗口的Cookie隔离);(2)浏览器地址栏是否显示http://而非https://(HTTPS下SameSite默认更严格);(3)是否清除了localhost的所有Cookie(旧Session干扰)。

这个过程之所以能成功,是因为浏览器在加载malicious.html里的iframe时,向transfer.php发起GET请求,自动附带了当前域(localhost)下所有有效的Cookie,包括那个维持登录态的PHPSESSID。而transfer.php的后端逻辑只检查$_SESSION['logged_in']是否存在且为true,完全不验证这个请求是不是用户主动发起的、来源是不是可信的。这就是CSRF的全部本质:利用浏览器的自动认证机制,完成一次用户不知情的状态变更。

2.3 攻击链路深度拆解:每一个HTTP包都在说什么

为了彻底看清发生了什么,我们必须抓包。用浏览器开发者工具(F12)的Network标签,或者Wireshark、Burp Suite等专业工具。以Chrome为例,在malicious.html加载时,Network面板会捕获到一条对transfer.php?action=transfer&to=hacker_wallet&amount=5000的GET请求。点开它,看Headers部分:

  • Request URL:http://localhost/csrf-lab/transfer.php?action=transfer&to=hacker_wallet&amount=5000

  • Request Method: GET

  • Status Code: 200 OK

  • Request Headers:

    • Cookie: PHPSESSID=abc123xyz← 这就是罪魁祸首!浏览器自动塞进来的Session标识
    • Referer: http://localhost/malicious.html← 来源是恶意页面,但后端没校验
    • User-Agent,Accept等常规字段
  • Response Headers:

    • Set-Cookie: PHPSESSID=abc123xyz; path=/; HttpOnly← Session继续有效

关键点来了:这个请求的Referer头明明写着malicious.html,为什么后端不拦住它?因为绝大多数老系统压根不读Referer,或者只做简单字符串匹配(如strpos($referer, 'localhost') !== false),而攻击者完全可以伪造Referer(通过Flash、Java Applet等,虽已淘汰,但原理相通)。更致命的是,有些API设计成允许跨域请求(CORS配置为Access-Control-Allow-Origin: *),此时Referer校验完全失效。

再对比一下正常用户点击transfer.php里那个GET链接时的请求:URL一模一样,Cookie一模一样,唯一区别是Referer是http://localhost/csrf-lab/transfer.php。但后端代码里没有任何逻辑去区分这两者。这就引出了第一个防御误区:仅靠Referer校验是脆弱的,因为它可以被客户端轻易篡改,且在某些网络环境下(如HTTPS→HTTP跳转)会被浏览器自动清除。

3. 防御方案实战:Token、SameSite与双重校验的落地细节

知道攻击怎么发生,只是第一步。真正的难点在于:如何在不破坏用户体验、不影响现有业务逻辑的前提下,堵住这个漏洞?很多团队的解决方案是“加个CSRF Token”,但加在哪?怎么生成?前端怎么传?后端怎么验?Token过期了怎么办?这些问题不厘清,防御就是纸糊的。下面我将基于前面搭建的靶场,逐一手把手实现三种主流防御方案,并指出每种方案在真实项目中踩过的坑。

3.1 方案一:同步令牌模式(Synchronizer Token Pattern)——最经典也最易翻车

这是OWASP官方推荐的首选方案。核心思想:每个敏感请求必须携带一个服务端生成、客户端提交、且无法被第三方预测的随机值(Token),服务端在处理请求前,先校验该Token是否有效且未被使用过。它不依赖浏览器特性,兼容性最好,但实现细节决定成败。

后端Token生成与校验(PHP实现)

修改transfer.php,在顶部加入Token生成逻辑:

<?php session_start(); if (!isset($_SESSION['logged_in']) || !$_SESSION['logged_in']) { header('Location: login.php'); exit; } // 生成CSRF Token:必须是加密安全的随机数,不能用time()或rand() if (!isset($_SESSION['csrf_token'])) { // PHP 7+ 推荐用random_bytes,兼容旧版可用openssl_random_pseudo_bytes $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // 64字符随机串 // 设置Token有效期,防止长期有效Token被窃取 $_SESSION['csrf_token_expires'] = time() + 3600; // 1小时 } // 校验Token的函数 function validateCsrfToken($token) { if (!isset($_SESSION['csrf_token']) || !isset($_SESSION['csrf_token_expires'])) { return false; } if (time() > $_SESSION['csrf_token_expires']) { // Token过期,销毁并返回false unset($_SESSION['csrf_token'], $_SESSION['csrf_token_expires']); return false; } // 使用hash_equals防止时序攻击(重要!) return hash_equals($_SESSION['csrf_token'], $token); } // 处理转账请求 if ($_POST['action'] === 'transfer' && isset($_POST['to']) && isset($_POST['amount'])) { // 关键:必须在业务逻辑前校验Token if (!validateCsrfToken($_POST['csrf_token'] ?? '')) { die('❌ CSRF Token validation failed!'); } $to = htmlspecialchars($_POST['to']); $amount = (int)$_POST['amount']; echo "<p style='color:green'>✅ Transfer successful! $amount to $to</p>"; file_put_contents('log.txt', date('Y-m-d H:i:s') . " - Transferred $amount to $to\n", FILE_APPEND); } ?>
前端Token注入(HTML模板)

在transfer.php的POST表单中,插入隐藏字段:

<form method="POST" action="transfer.php"> <input type="hidden" name="action" value="transfer"> <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>"> <input type="text" name="to" placeholder="Recipient" required> <input type="number" name="amount" placeholder="Amount" required> <button type="submit">Send via POST</button> </form>
实测与避坑心得

部署后,用Burp Suite拦截POST请求,手动删掉csrf_token字段或改成错误值,会立即看到❌ CSRF Token validation failed!。这说明防御生效了。但问题接踵而至:

  • 坑1:Token刷新时机不对。很多团队把Token生成放在登录成功后,之后再也不更新。结果用户登录后挂机8小时,Token过期了,他点提交就报错。正确做法是:每次渲染包含敏感操作的页面时,都生成新Token,并设置合理过期时间(30分钟~2小时)。上面代码里if (!isset($_SESSION['csrf_token']))的判断,就是为了避免重复生成,但实际项目中建议每次页面加载都重置。

  • 坑2:AJAX请求的Token传递被忽略。现代应用大量用AJAX,而Token必须随每个请求发送。常见错误是只在表单里加了隐藏字段,但JS里fetch()或$.ajax()时忘了把Token塞进headers。解决方案:统一在JS入口处读取页面某个meta标签的content值,或从全局变量获取,然后自动添加到所有AJAX请求头中:

<!-- 在页面<head>里 --> <meta name="csrf-token" content="<?php echo $_SESSION['csrf_token']; ?>">
// JS中统一设置 const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); fetch('/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken // 后端需从此header读取 }, body: JSON.stringify({to: 'xxx', amount: 100}) });
  • 坑3:Token存储位置不当。把Token存在localStorage或普通Cookie里,等于白加——XSS漏洞可以直接读取。必须存在HttpOnly Cookie里(服务端生成),并通过表单字段或自定义Header传递。上面PHP代码中Token存在Session里,由服务端控制,是最安全的。

3.2 方案二:SameSite Cookie属性——浏览器原生防线,但有陷阱

SameSite是HTTP Cookie的一个属性,用于指示浏览器在何种情况下应该发送该Cookie。它有三个值:Strict、Lax、None。这是浏览器层面的防御,无需修改业务代码,但配置不当反而会阻断正常功能。

SameSite=Lax:平衡安全与可用性的默认选择

修改login.php中的Session启动方式,强制设置SameSite:

<?php // PHP 7.3+ 可直接在session_set_cookie_params中设置 session_set_cookie_params([ 'lifetime' => 0, 'path' => '/', 'domain' => '', 'secure' => false, // 本地测试用HTTP,设为false;生产环境HTTPS必须true 'httponly' => true, 'samesite' => 'Lax' // 关键:设置SameSite属性 ]); session_start(); // ...后续登录逻辑

或者,在PHP 7.2及以下版本,用setcookie手动设置:

session_start(); $params = session_get_cookie_params(); setcookie( session_name(), session_id(), [ 'expires' => $params['lifetime'] ? time() + $params['lifetime'] : 0, 'path' => $params['path'], 'domain' => $params['domain'], 'secure' => $params['secure'], 'httponly' => $params['httponly'], 'samesite' => 'Lax' ] );
SameSite=Lax如何阻断CSRF?

当SameSite=Lax时,浏览器规则是:只有GET请求(且是用户主动导航,如点击链接、地址栏输入)才会发送Cookie;所有跨站POST请求、iframe加载、img/script标签的GET请求,都不会携带Cookie。回到我们的攻击场景:malicious.html里的iframe尝试GETtransfer.php,由于是跨站(malicious.htmlvslocalhost/csrf-lab),且不是用户主动导航,浏览器直接不发送PHPSESSID Cookie。结果transfer.php里$_SESSION['logged_in']为false,重定向到登录页,攻击失败。

生产环境的致命陷阱

SameSite看似完美,但在真实项目中极易翻车:

  • 陷阱1:混合协议导致Cookie丢失。如果前端是https://app.example.com,后端API是http://api.example.com(HTTP),那么SameSite=Lax在Chrome下会直接拒绝发送Cookie,因为协议不一致被视为跨站。解决方案:前后端必须同协议,且生产环境强制HTTPS。

  • 陷阱2:移动端WebView兼容性问题。iOS 12之前的WKWebView、Android 7以下的WebView不支持SameSite属性,会直接忽略。这意味着在这些老旧环境中,SameSite防御形同虚设。必须配合服务端Token方案作为兜底。

  • 陷阱3:Lax模式对“用户主动导航”的定义模糊。Chrome认为<a href="...">是主动,但<form method="GET">提交不算。所以如果攻击者用<form action="http://victim.com/transfer" method="GET"><input type="submit"></form>,用户点击提交,仍会发送Cookie!这就是为什么Lax不能防御所有GET型CSRF,必须禁止GET请求执行状态变更(即:转账、删除、修改等操作必须用POST/PUT/DELETE)。

3.3 方案三:双重提交Cookie模式——轻量级补充,但需警惕逻辑漏洞

双重提交Cookie(Double Submit Cookie)是一种不依赖服务端状态的方案:服务端生成一个随机Token,同时写入Cookie和请求参数(如表单字段或Header),服务端只需比对两者是否一致即可。它比同步令牌轻量(不用存Session),但安全性略低。

实现步骤
  1. 用户登录后,服务端生成Token并设为Cookie:
$token = bin2hex(random_bytes(16)); setcookie('XSRF-TOKEN', $token, ['path' => '/', 'samesite' => 'Lax']);
  1. 前端在每个敏感请求中,将该Token放入请求头(如X-XSRF-TOKEN)或表单字段。

  2. 后端校验:

if (!hash_equals($_COOKIE['XSRF-TOKEN'] ?? '', $_SERVER['HTTP_X_XSRF_TOKEN'] ?? '')) { die('Invalid XSRF token'); }
为什么说它“轻量但有风险”?
  • 优势:不依赖服务端存储,适合无状态微服务架构;Token随Cookie自动发送,前端只需读取并转发。
  • 风险:如果攻击者能通过XSS窃取Cookie(如未设HttpOnly),就能同时拿到Token和Cookie,防御失效。因此,必须确保Cookie设置了HttpOnly和Secure(HTTPS下)。另外,Token不能有业务含义(如不能是用户ID加密),否则可能被预测。

在我们的靶场中,可以把它作为SameSite的补充:即使SameSite被绕过(如旧浏览器),双重提交仍能拦截。但绝不能单独使用。

4. 绕过手法与防御对抗:当攻击者开始研究你的防御

安全是一个攻防对抗的过程。当你以为加了Token就万事大吉时,攻击者已经在研究如何绕过。在真实渗透测试中,我见过太多“看似修复了CSRF,实则形同虚设”的案例。下面复现几种经典绕过手法,不是为了教你攻击,而是让你看清防御的脆弱点,从而加固自己的系统。

4.1 Token泄露:从Referer头到前端调试信息

CSRF Token的安全前提是“攻击者无法获取它”。但很多应用无意中把它泄露给了第三方。

  • Referer泄露:某电商后台的登录页,会把CSRF Token拼在URL里:login.php?token=abc123。当用户点击站内链接跳转到合作方网站时,浏览器会把完整URL(含token)作为Referer头发送过去。合作方服务器日志里就存下了这个Token。修复方法:Token永远不要出现在URL中,只通过POST body、Header或隐藏表单字段传递。

  • 前端调试信息泄露:某金融App的Vue组件里,把Token存在data对象中,并在控制台打印了整个this对象。攻击者只需在页面里注入一段JS:console.log(document.querySelector('#app').__vue__.$data.csrfToken),就能拿到Token。修复方法:生产环境移除所有调试代码;敏感数据不要挂在Vue/React实例上;使用Object.freeze()冻结敏感对象。

  • 错误页面泄露:某CMS的Token校验失败时,返回500错误页,并在HTML注释里写了<!-- Debug: expected token=abc123, got=def456 -->。攻击者用Burp反复爆破,就能收集到有效Token。修复方法:错误页面绝不返回任何调试信息;统一用403 Forbidden,不解释原因。

4.2 Token同步失效:AJAX请求中的“幽灵Token”

这是最隐蔽的坑。假设你的应用用Vue开发,所有API都走axios。你在main.js里写了:

axios.interceptors.request.use(config => { config.headers['X-CSRF-TOKEN'] = getCsrfToken(); // 从某个地方读取 return config; });

看起来很完美。但问题在于:getCsrfToken()函数如果从document.cookie读取,而Token是服务端通过Set-Cookie下发的,那么当Token过期后,服务端会下发新Cookie,但前端JS读取的还是旧值(因为document.cookie不会自动更新)。结果所有AJAX请求都带着过期Token,全部403。用户刷新页面才恢复。

真实案例:某在线教育平台,学生上课时长超过1小时,Token过期,但前端没感知,导致交卷失败,大量客诉。解决方案:Token必须和服务端Session绑定,前端每次发请求前,先用fetch('/api/csrf-token')异步获取最新Token,再发业务请求;或者服务端在响应头中返回新Token(如X-CSRF-Token: new_value),前端JS监听并更新本地缓存。

4.3 SameSite绕过:当“Lax”变成“Lax but not really”

SameSite=Lax本意是好的,但浏览器实现有灰色地带。Chrome有一个鲜为人知的行为:如果用户在30秒内连续两次从同一站点A导航到站点B,第二次的跨站请求会豁免SameSite限制,发送Cookie。攻击者可以利用这点:

  1. 诱导用户访问attacker.com/step1.html,其中用<iframe src="https://victim.com/login">加载登录页(用户看到的是victim.com,以为在正常登录);
  2. 用户输入账号密码,登录成功(此时victim.com的Cookie已建立);
  3. step1.html立即跳转到attacker.com/step2.html,其中嵌入<iframe src="https://victim.com/transfer?to=attacker&amount=10000">;
  4. 由于两次导航间隔极短,Chrome认为这是“用户意图连续操作”,放行Cookie。

这个绕过需要用户配合登录,但成功率很高。防御方法:SameSite=Lax必须配合其他方案(如Token),不能单独依赖;对高危操作(如转账),强制二次验证(短信/邮箱验证码)。

5. 渗透测试实战 checklist:从发现到验证的完整闭环

在真实渗透测试中,发现CSRF漏洞不是终点,验证其危害、评估影响范围、给出可落地的修复建议,才是价值所在。下面是我总结的一套标准化checklist,已在数十个中大型项目中验证有效,覆盖从自动化扫描到手工验证的全流程。

5.1 自动化扫描:快速定位高危接口

别迷信商业扫描器。用开源工具组合拳更高效:

  • Arjun:专攻参数发现。对目标站点跑arjun -u https://target.com/api/ --get,找出所有带参数的GET接口。重点标记那些参数名含delete、transfer、update、enable、disable的URL。
  • ffuf:暴力探测敏感路径。ffuf -u https://target.com/FUZZ -w /path/to/common.txt -t 100,字典里包含/api/logout、/user/settings、/admin/delete等。
  • Burp Suite Intruder:对疑似接口批量测试。设置Payload为GET /api/transfer?to=attacker&amount=1 HTTP/1.1\r\nHost: target.com\r\n\r\n,观察响应码是否为200/302,且响应体含success、ok等关键词。

注意:自动化扫描只能发现“可能存在的CSRF”,不能确认“一定能利用”。必须手工验证。

5.2 手工验证四步法:拒绝假阳性

我坚持的手工验证流程,确保每个标为“高危”的漏洞都经得起推敲:

  1. 确认会话依赖:用Burp Proxy拦截一个正常业务请求(如转账),复制其Cookie,用curl重放:curl -H "Cookie: sessionid=xxx" "https://target.com/api/transfer?to=test&amount=1"。如果返回成功,说明该接口确实只依赖Cookie,不校验其他因素(如IP、User-Agent)。

  2. 构造跨站请求:新建一个本地HTML文件,用<img>、<iframe>、<form>三种方式分别请求该URL。例如:

    <img src="https://target.com/api/transfer?to=hacker&amount=1000" width="1" height="1">

    加载后,检查目标应用的后台日志或数据库,确认操作是否真实执行。

  3. 验证Referer/Origin校验:在Burp中修改请求头,将Referer: https://target.com改为Referer: https://evil.com,Origin: https://target.com改为Origin: https://evil.com,重放。如果仍成功,说明服务端没校验这两个头。

  4. 检查Token机制:查看页面源码,搜索csrf、token、_token等关键词。如果存在,尝试删除该参数或篡改值,看是否报错。如果没找到,基本可判定无Token防护。

5.3 影响评估与修复建议:让开发同学愿意改

渗透报告的价值,不在于标多少个“高危”,而在于推动修复。我的建议永远遵循三个原则:可验证、可落地、最小改动。

  • 可验证:明确告诉开发,“修复后,用这个curl命令重放,应该返回403”。而不是模糊地说“加强校验”。

  • 可落地:针对不同技术栈给出具体代码。例如:

    • Spring Boot:@EnableWebSecurity类中加http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    • Django:确保settings.py中CSRF_COOKIE_SECURE=True(HTTPS下),CSRF_TRUSTED_ORIGINS=['https://yourdomain.com'];
    • Node.js (Express):用csurf中间件,但必须配合res.cookie('XSRF-TOKEN', token, { httpOnly: false, sameSite: 'lax' })。
  • 最小改动:优先推荐SameSite=Lax(改一行配置),其次同步令牌(改两处:后端生成+前端提交),最后才建议重构为OAuth2等重型方案。毕竟,让一个运行5年的老系统一夜之间接入OAuth,不如先堵住眼前的洞。

最后分享一个血泪教训:某政务系统渗透,我发现一个/api/user/delete?id=123的GET接口存在CSRF。按流程提交报告,开发回复“这个接口只有管理员能访问,且前端做了权限控制,没问题”。我反手用Burp发了一个GET /api/user/delete?id=123 HTTP/1.1请求,Header里只带Cookie,没带任何权限头,结果返回200。追问后才知道,他们的“权限控制”只在前端JS里判断if (user.role === 'admin'),后端根本没校验。所以,CSRF测试必须绕过前端,直击后端逻辑。这也是为什么,所有手工验证的第一步,永远是curl重放。

我在实际操作中发现,最有效的CSRF防御,从来不是某一个银弹方案,而是分层防御的组合拳:浏览器层(SameSite)、传输层(HTTPS强制)、应用层(Token+Referer校验)、业务层(高危操作二次验证)。每一层都可能被绕过,但叠加起来,攻击成本就指数级上升。就像修一道墙,单层砖容易被凿穿,但加上钢筋、混凝土、电子围栏,想突破就得动用挖掘机——而绝大多数脚本小子,连扳手都懒得找。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 5:06:45

Qoder 上手实战:AI 编程 IDE 的 Agent 与 MCP 接入指南

1. 为什么大家都在找 Codex 的平替AI 编程工具这个赛道&#xff0c;从 2024 年下半年开始就进入了白热化阶段。最早大家用的是 Copilot 那种行内补全&#xff0c;后来 Cursor 把「对话式改代码」带火了&#xff0c;再往后 Codex 这类云端 Agent 工具把「你说需求、它自己跑命令…

作者头像 李华
网站建设 2026/10/17 5:06:08

网红水单背后:叛逆饮的产品研发、命名套路与传播链路

这两年如果你留心各处的饮品店吧台&#xff0c;会发现一件事&#xff1a;菜单上冒出越来越多“不像饮料”的饮料。香菜、折耳根、花椒、老姜、木姜子、黑蒜甚至带着中药味的陈皮、甘草&#xff0c;都开始往杯子里跑。名字也从“招牌奶茶”“经典美式”变成“暗黑青花椒气泡美式…

作者头像 李华
网站建设 2026/10/17 5:05:50

MySQL性能调优实战:慢SQL定位、索引优化与参数调整全复盘

1. 为什么我的MySQL突然变慢了&#xff1a;一次线上故障的复盘大概半年前&#xff0c;我负责维护的一个业务系统突然出现了接口响应时间暴涨的情况。监控面板显示数据库连接数飙升&#xff0c;CPU打满&#xff0c;应用层的慢请求日志里一片飘红。我一开始还以为是代码问题&…

作者头像 李华
网站建设 2026/10/17 5:05:47

Mac上安装OpenClaw自动化工具:从环境准备到配置运行的完整实战指南

1. 写在动手之前&#xff1a;为什么这篇教程值得你读完先说明一下&#xff0c;这个 OpenClaw&#xff0c;早期叫 Clawdbot&#xff0c;是一个典型的“拿来就能用”的跨平台自动化任务处理工具。它解决的核心痛点很简单&#xff1a;你手头有一堆重复性的、规则明确的操作&#x…

作者头像 李华
网站建设 2026/10/17 5:05:44

Claude Code 接入 Opus 4.8:从安装配置到模型切换避坑全攻略

Claude Code 接入 Opus 4.8 这件事&#xff0c;2026 年开年以来的讨论热度一直没降。安装、配置、模型切换&#xff0c;三件事单看都不算复杂&#xff0c;但合在一起实操&#xff0c;真正顺利一次走通的人并不多。我前前后后帮团队搭过几套环境&#xff0c;也看了不少社区反馈&…

作者头像 李华
网站建设 2026/10/17 5:05:44

HFOA-CNN-BiLSTM-Attention多变量回归预测与消融对比

四模型放在一起跑回归预测&#xff0c;最容易被忽略的问题是&#xff1a;你算出来的那个精度&#xff0c;到底是谁的功劳&#xff1f;是注意力机制起了作用&#xff0c;还是BiLSTM本身足够强&#xff0c;还是优化算法把参数调到了一个幸运值&#xff1f;如果不对这个问题较真&a…

作者头像 李华