CSRF攻击原理与防御实战:从同源策略到Token验证

发布时间:2026/6/30 8:15:59
CSRF攻击原理与防御实战:从同源策略到Token验证 1. 项目概述从一次“被点赞”说起那天下午我正和团队开会手机突然弹出一条通知“您刚刚点赞了XX博主的视频”。我心里咯噔一下我明明在开会手机就放在桌上怎么可能去操作点开App查看发现不仅点了赞还关注了几个完全不认识的账号。第一反应是账号被盗了赶紧改密码、查登录记录结果一切正常。后来在安全团队的协助下排查才发现问题出在一个我前几天访问过的、看似正常的第三方网站上。我的浏览器在不知情的情况下向视频平台发送了一个“点赞”请求而平台服务器认为这就是我本人的操作。这就是一次典型的CSRF攻击。CSRF全称Cross-Site Request Forgery中文常译为“跨站请求伪造”。这个名字听起来有点学术但它的本质非常“流氓”攻击者欺骗用户的浏览器让其以用户的名义向一个用户已经认证过的网站发起非本意的请求。简单来说就是“借刀杀人”——利用你在A网站比如视频平台的登录状态在B网站恶意网站上偷偷让你的浏览器去A网站干坏事。你本人毫无感知但“坏事”已经做成了比如转账、改密码、发微博、买商品。CSRF的危害远不止“被点赞”这么简单它直接威胁到用户的数据安全、财产安全和账号主权是Web安全领域必须筑牢的一道防线。2. CSRF攻击的核心原理与危害深度解析2.1 “信任”与“身份”的错位CSRF如何发生要理解CSRF必须抓住两个核心浏览器的同源策略对请求的限制以及Web应用的身份认证机制。首先浏览器的同源策略Same-Origin Policy主要限制的是不同源站点之间如何读取对方的资源如Cookie、DOM。但它有一个关键“后门”对于向服务器发送请求这个动作限制要宽松得多。一个来自evil.com的页面可以自由地向bank.com发起一个GET、POST甚至其他方法的HTTP请求。浏览器不会因为这个请求的发起方是evil.com就阻止它发往bank.com。其次Web应用普遍使用会话Session机制来维持用户登录状态。最常见的实现方式就是在用户登录成功后服务器生成一个唯一的Session ID通过Set-Cookie头返回给浏览器。浏览器之后向该域名下的任何请求都会自动带上这个Cookie。服务器通过验证Cookie中的Session ID来确认“这是用户张三本人”。CSRF攻击正是利用了这两点的结合用户已登录用户访问了bank.com并成功登录浏览器保存了该站点的登录Cookie。用户访问恶意站点用户在同一浏览器中打开了攻击者构造的恶意页面evil.com。恶意页面发起伪造请求evil.com的页面中包含一个会自动向bank.com发起请求的代码比如一个隐藏的img标签其src指向银行的转账接口或者一个自动提交的form。浏览器自动携带Cookie浏览器向bank.com发起这个请求时会自动、无声地附带上bank.com的登录Cookie因为这是同域请求的默认行为。服务器被欺骗bank.com的服务器收到请求看到合法的Session Cookie便认为这是用户张三的合法操作于是执行了转账、改密等指令。整个过程用户完全不知情攻击者也没有窃取到用户的密码或Cookie。他仅仅是“借用”了浏览器对用户的信任。2.2 不止于财产CSRF攻击的多维危害很多人一提到CSRF就想到盗刷、转账但其危害面要广得多主要体现在以下几个层面2.2.1 直接财产损失这是最直观的危害。攻击者可以伪造请求让用户在不知情的情况下进行资金操作。电商场景伪造“确认收货”请求在用户未收到货时完成交易或伪造“使用优惠券购买高价商品”的请求。金融场景伪造转账请求将用户账户内的资金转到攻击者账户。即使有短信验证码在某些设计不当的二次确认环节也可能被绕过。虚拟资产在游戏平台伪造购买道具、赠送资产的请求。2.2.2 数据与隐私泄露CSRF不仅可以“写”数据也能诱导“读”操作结合其他漏洞造成数据泄露。篡改个人资料伪造请求修改用户的邮箱、手机号、密保问题。攻击者随后可以通过“找回密码”功能接管账号。泄露敏感信息在某些设计不当的API中修改用户设置如公开性设置的请求可能是GET方式攻击者可以诱导用户访问一个链接将其私密内容设置为公开。结合XSS扩大危害如果站点同时存在XSS漏洞攻击者可以先通过XSS获取到CSRF Token一种防CSRF的令牌再构造完美的CSRF攻击防不胜防。2.2.3 账号安全体系崩塌CSRF能直接威胁账号的根基。修改密码这是最致命的攻击之一。攻击者伪造修改密码的请求成功后用户将无法登录自己的账号账号完全被接管。修改绑定邮箱/手机为后续的完全控制铺平道路。注销或禁用账号伪造账号注销请求给用户带来巨大麻烦。2.2.4 业务逻辑与声誉损害社交平台伪造“发布状态”、“点赞”、“关注”、“拉黑”等请求。就像我开篇的经历污染用户的社交形象或进行恶意营销。内容管理系统伪造“发布文章”、“删除文章”、“修改配置”等请求影响网站内容安全。企业内网应用伪造OA系统、运维系统的操作请求如审批流程、服务器操作指令可能引发内部管理混乱甚至安全事故。声誉风险用户因遭受损失而对平台产生信任危机平台可能面临法律诉讼和舆论压力。注意CSRF攻击的成功有一个关键前提即被攻击的请求缺乏对请求来源的验证。服务器只认“Cookie”这个身份凭证而不关心这个带着凭证的请求是从哪来的。这就像门卫只认工牌Cookie但不检查持工牌的人是不是从公司大门合法页面进来的还是从围墙外扔进来的恶意页面。3. 实战推演攻击者如何构造一个CSRF攻击理解了原理我们来看看攻击者具体是怎么做的。这里以最常见的GET和POST请求为例进行拆解。我们会用到像Pikachu、DVWA、74cms这类靶场环境因为它们提供了清晰且合法的漏洞场景供学习。3.1 针对GET请求的CSRF攻击GET请求的参数通常附在URL上构造起来最简单。假设一个脆弱的银行转账接口如下http://vulbank.com/transfer?toaccount_bamount1000攻击者构造的恶意页面可能只是一个隐藏的图片标签!-- 恶意页面 evil.html 内容 -- html body h1恭喜你中奖了/h1 img srchttp://vulbank.com/transfer?tohacker_accountamount10000 width0 height0 / /body /html当已登录vulbank.com的用户访问这个evil.html时浏览器会尝试加载那个“图片”从而向银行服务器发起一个转账给hacker_account10000元的GET请求。由于携带了Cookie服务器会执行。在DVWADamn Vulnerable Web Application靶场的Low难度CSRF关卡中修改密码的接口就是GET请求。攻击页面可以是一个短链接或者论坛里的一张“有趣”图片的链接用户一点击密码就被改掉了。实操心得GET请求用于变更操作是绝对的反模式。这是Web开发的基本原则之一。GET请求应该是幂等的、安全的仅用于获取资源不应产生副作用。即使前端用JavaScript发起POST如果后端没有正确校验攻击者同样可以构造一个自动提交的Form来模拟POST请求。3.2 针对POST请求的CSRF攻击POST请求更常见但同样不安全。假设修改邮箱的接口是一个POST表单!-- 正常修改邮箱的表单 -- form action/change_email methodPOST input typeemail namenew_email valueusernormal.com input typesubmit value确认修改 /form攻击者构造的恶意页面会包含一个自动提交的隐藏表单!-- 恶意页面 evil-post.html 内容 -- html body onloaddocument.forms[0].submit() form actionhttp://vulbank.com/change_email methodPOST styledisplay:none; input typehidden namenew_email valuehackerevil.com /form p页面加载中请稍候.../p /body /html当用户访问该页面onload事件会触发表单自动提交。表单数据new_emailhackerevil.com会以POST方式发送到银行服务器并携带用户的登录Cookie。在Pikachu靶场的CSRF关卡中就有类似的POST型漏洞演示。攻击者需要精确知道请求的参数名如new_email但这通常通过分析正常请求或前端代码很容易获得。3.3 高级与变种攻击手法JSON CSRF随着前后端分离和RESTful API流行很多操作通过发送JSON数据进行。如果服务器端没有严格检查Content-Type头例如只处理application/json攻击者有可能通过构造一个form并利用某些浏览器的特性以application/x-www-form-urlencoded格式发送数据如果后端解析逻辑不严谨可能成功。通过XSS绕过Token防御如果网站同时存在存储型XSS漏洞攻击者可以先注入恶意脚本。当用户访问存在XSS的页面时脚本可以窃取页面中生成的CSRF Token然后动态构造一个携带正确Token的伪造请求。这种组合拳危害极大。利用用户交互的“点击劫持”将恶意页面通过iframe透明覆盖在另一个诱人的按钮如“点击抽奖”之上。用户以为自己点击的是抽奖按钮实际点击的是iframe内“确认转账”的按钮。这需要结合UI上的欺骗。排查技巧在渗透测试或代码审计时寻找CSRF漏洞的关键是识别所有状态变更的端点登录、注销、修改、删除、支付等然后检查它们是否缺乏可靠的来源验证机制。工具如Burp Suite的“Engagement tools” - “Generate CSRF PoC”可以快速为找到的请求生成攻击测试页面。4. 构建防线多层次CSRF防御实战指南防御CSRF的核心思想是打破“请求自动携带Cookie”即可代表用户意愿这个假设。我们需要让服务器有能力区分“来自用户真实操作的请求”和“来自恶意第三方页面的请求”。4.1 黄金标准CSRF Token同步模式这是目前最主流、最有效的防御方案。其原理是服务器在用户会话中生成一个随机、不可预测的令牌Token同时将这个Token放入即将返回给用户的表单或页面中作为一个隐藏字段。当用户提交表单时这个Token会随着表单数据一起提交回服务器。服务器收到请求后比对请求中的Token和会话中存储的Token是否一致。只有一致才认为是合法请求否则拒绝执行。为什么有效因为恶意网站evil.com无法知道这个随机Token的值受同源策略保护它读不到bank.com页面里的内容因此无法在伪造的请求中携带正确的Token。服务端实现示例以Node.js/Express为例// 1. 生成并存储Token可以使用crypto模块 const crypto require(crypto); function generateCsrfToken(req) { if (!req.session.csrfToken) { req.session.csrfToken crypto.randomBytes(32).toString(hex); } return req.session.csrfToken; } // 2. 渲染页面时注入Token app.get(/transfer, (req, res) { const csrfToken generateCsrfToken(req); res.render(transfer-form, { csrfToken: csrfToken }); }); // 在 transfer-form.ejs 模板中 form action/do-transfer methodPOST input typehidden name_csrf value% csrfToken % !-- 其他表单字段 -- input typetext nametoAccount input typenumber nameamount button typesubmit转账/button /form // 3. 验证Token的中间件 app.post(/do-transfer, (req, res, next) { const userToken req.body._csrf; const sessionToken req.session.csrfToken; if (!userToken || userToken ! sessionToken) { return res.status(403).send(CSRF Token验证失败); } // Token验证通过执行转账逻辑 // ... // 可选使用后使当前Token失效生成新的更安全但可能影响多标签页 // req.session.csrfToken crypto.randomBytes(32).toString(hex); });前端AJAX请求如何携带Token对于单页应用SPAToken通常可以放在一个全局的meta标签里或者由登录接口返回。在发起AJAX请求时将其作为自定义请求头如X-CSRF-Token发送。// 从meta标签获取Token const csrfToken document.querySelector(meta[namecsrf-token]).getAttribute(content); // 使用Fetch API发起请求 fetch(/api/transfer, { method: POST, headers: { Content-Type: application/json, X-CSRF-Token: csrfToken // 关键将Token放在自定义头中 }, body: JSON.stringify({ to: account_b, amount: 100 }) });服务器端则需要检查这个自定义头部的值。注意事项与避坑指南Token的强度必须使用密码学安全的随机数生成器CSPRNG长度足够如32字节防止被爆破。Token的存储应存储在服务器端的Session中绝不能仅放在Cookie里返回给客户端。因为Cookie会被浏览器自动携带攻击者伪造的请求也能带上它失去了防御意义。Token的绑定Token最好能与用户会话甚至具体操作绑定。例如为每个表单生成唯一的Token而不是整个会话共用一个。Token的过期与更新可以考虑在每次使用后使旧Token失效生成新Token同步Token模式。但这会带来多标签页操作的兼容性问题一个标签页使用了Token导致其他标签页的Token失效。折中方案是让Token有一定有效期或采用“双Token”等更复杂的方案。不要依赖请求头Referer/Origin虽然它们有用但不能作为唯一防线因为某些浏览器隐私设置或网络代理可能会剥离这些头部。4.2 重要补充双重Cookie验证这种方案利用了“攻击者可以发起请求但无法直接读取目标站点的Cookie值”这一特点。前端在请求时从Cookie中读取某个自定义的Token值例如CSRF-TOKENrandom_value。将这个Token值作为请求参数如_csrf或自定义请求头如X-CSRF-Token的一部分随请求一起发送。后端同时验证请求中的Cookie里的Token和参数/头里的Token是否一致。为什么有效恶意页面可以发起请求并自动携带Cookie但它无法通过JavaScript读取到目标站点Cookie的具体内容同源策略限制因此无法将正确的Token值放到参数或头里。后端比对会发现不一致从而拒绝请求。实现示例// 登录成功后设置一个自定义的Cookie res.cookie(CSRF-TOKEN, randomToken, { httpOnly: false }); // 注意这里httpOnly需要为false让JS能读到 // 前端JavaScript发起请求前 function getCookie(name) { const value ; ${document.cookie}; const parts value.split(; ${name}); if (parts.length 2) return parts.pop().split(;).shift(); } const csrfToken getCookie(CSRF-TOKEN); // 发起请求将Token放在头部 fetch(/api/action, { method: POST, headers: { X-CSRF-Token: csrfToken }, body: JSON.stringify(data) }); // 后端验证 app.post(/api/action, (req, res) { const cookieToken req.cookies[CSRF-TOKEN]; const headerToken req.headers[x-csrf-token]; if (!cookieToken || cookieToken ! headerToken) { return res.status(403).send(CSRF验证失败); } // 验证通过 });优缺点优点实现相对简单对前端框架侵入性小适合API场景。缺点需要将Cookie设置为httpOnly: false这给了XSS攻击窃取Token的机会如果存在XSS漏洞。如果主域名存在子域名污染风险例如a.example.com可以设置example.com的Cookie攻击者可能利用此漏洞设置一个已知的Token。因此双重Cookie验证通常作为辅助或备用方案不建议作为唯一防线。4.3 同源检测利用Referer与Origin头部HTTP请求头中的Referer或更标准的Origin表明了请求的来源页面。服务器可以检查这个值如果来源不是自己信任的域名则拒绝请求。Origin头用于跨域请求CORS标明请求发起的原始站点协议域名端口。对于同源POST请求浏览器也可能不发送Origin。Referer头包含完整的来源页面URL。但隐私敏感用户或防火墙可能禁用它。服务器端校验示例function checkOrigin(req, res, next) { const allowedOrigins [https://www.mybank.com, https://mybank.com]; const requestOrigin req.get(Origin) || req.get(Referer); if (requestOrigin) { const originUrl new URL(requestOrigin); if (!allowedOrigins.includes(originUrl.origin)) { return res.status(403).send(非法请求来源); } } else { // 对于没有Origin/Referer的直接请求如从地址栏输入、书签访问需要结合其他策略如Token // 可能是合法的GET请求也可能是伪造的请求。需谨慎处理。 } next(); } // 将此中间件应用于需要保护的路由 app.post(/sensitive-action, checkOrigin, (req, res) { /* ... */ });注意事项不能作为唯一依赖Referer头可能被篡改尽管浏览器通常不允许也可能被隐私设置或安全软件剥离。Origin头在简单的跨站请求如form提交中可能不存在。空Referer的处理需要定义策略。对于直接输入URL或从本地文件发起的请求Referer可能为空。这需要根据业务场景判断是否放行。通常对于关键操作空Referer应拒绝。最佳实践将同源检测作为深度防御Defense in Depth的一环与CSRF Token结合使用。4.4 框架内置防御与最佳实践现代Web开发框架几乎都内置了CSRF防护。Django中间件django.middleware.csrf.CsrfViewMiddleware默认启用为表单提供{% csrf_token %}标签。Spring Security (Java)默认启用CSRF保护要求所有非GET、HEAD、TRACE、OPTIONS的请求必须携带_csrf参数或X-CSRF-TOKEN头。Laravel (PHP)为每个用户会话生成CSRF Token通过csrf指令在表单中插入。Express (Node.js)可以使用csurf中间件注意其维护状态或自行实现如上所述的Token逻辑。使用框架时的注意事项理解豁免场景为某些API如对外开放的、无状态的RESTful API正确配置CSRF豁免。AJAX集成确保前端AJAX库如Axios正确配置能自动携带Token。Cookie配置确保Session Cookie的SameSite属性设置为Strict或Lax见下文这是一道重要的辅助防线。4.5 浏览器安全特性SameSite Cookie属性这是近年来对抗CSRF的一大利器。通过设置Cookie的SameSite属性可以控制浏览器在跨站请求时是否发送Cookie。SameSiteStrict最严格。Cookie仅在同站请求即当前页面URL与请求目标URL的eTLD1相同时发送。完全阻止CSRF但可能导致用户体验问题例如从邮件链接点击进入网站登录状态会丢失。SameSiteLax现代浏览器的默认值宽松模式。允许在顶级导航如点击链接的GET请求中发送Cookie但阻止在跨站的子请求如图片、iframe、AJAX中发送。这能防御大多数CSRF攻击因为CSRF通常通过img,form,fetch等方式发起同时保持了基本的用户体验。SameSiteNoneCookie在所有上下文中发送。必须与Secure属性仅限HTTPS一起使用。适用于需要跨站共享状态的第三方服务。设置示例Node.jsres.cookie(sessionId, sessionToken, { httpOnly: true, secure: true, // 仅HTTPS sameSite: lax // 或 strict });实操心得将SameSiteLax作为所有会话Cookie和身份验证Cookie的默认配置能极大地缓解CSRF风险且对用户影响最小。但这不能替代CSRF Token因为SameSiteLax对某些场景如跨站POST表单提交在某些旧版浏览器或特定配置下可能不提供保护。它应该被视为一道重要的、补充性的防线。5. 防御体系构建与渗透测试自查清单一套健壮的CSRF防御体系应该是多层次、纵深防御的。以下是我在实际项目中的构建和检查经验。5.1 企业级CSRF防御架构建议第一层核心防御CSRF Token强制实施对所有状态变更的请求非幂等的POST、PUT、PATCH、DELETE实施CSRF Token校验。安全生成使用密码学安全的随机数生成器。安全存储Token存储在服务器端Session中或使用加密签名的方式下发给客户端如JWT形式但需注意刷新机制。前端安全集成确保单页应用SPA能方便地获取和携带Token通过Meta标签、初始状态注入等。第二层关键补充SameSite Cookie属性默认配置将所有关键的会话Cookie、认证Cookie设置为SameSiteLax; Secure; HttpOnly。例外处理对于确实需要跨站使用的第三方集成Cookie明确设置为SameSiteNone; Secure并评估其风险。第三层深度检测请求来源校验校验Origin/Referer头在服务器端中间件中对敏感请求校验Origin或Referer头确保来源是受信任的域名列表。将此作为Token验证前的一道快速过滤网。处理边缘情况制定明确的策略处理空Referer的请求例如对于关键操作直接拒绝对于非关键操作可结合其他因素判断。第四层降低影响关键操作二次确认业务逻辑加固对于特别敏感的操作如转账、修改密码、修改邮箱强制要求进行二次验证。这可以是重新输入密码。输入手机/邮箱验证码。使用生物识别指纹、人脸。注意二次确认的请求本身也必须受到CSRF保护否则攻击者可以连续伪造两个请求。第五层持续监控安全扫描与日志审计自动化扫描将CSRF漏洞检测纳入CI/CD流水线使用SAST静态应用安全测试和DAST动态应用安全测试工具定期扫描。关键操作日志详细记录所有敏感操作的日志包括用户ID、时间、IP、User-Agent、操作类型和参数脱敏后。这有助于在事件发生后进行追溯和分析。异常行为告警监控短时间内同一用户、同一IP的高频敏感操作建立告警机制。5.2 开发者自查与渗透测试清单在代码开发、代码审查或渗透测试时可以对照以下清单进行检查代码审计自查清单[ ]识别端点是否列出了所有进行状态变更增删改的HTTP端点[ ]Token验证上述端点是否都实现了CSRF Token验证验证逻辑是否正确比较服务器Session与请求参数/头[ ]Token安全性Token是否足够随机、唯一是否每会话或每表单生成[ ]豁免检查对于确实不需要CSRF保护的公开API如登录接口本身、部分第三方回调是否已正确配置豁免豁免理由是否充分[ ]Cookie设置会话Cookie是否设置了HttpOnly、Secure和SameSiteLax/Strict属性[ ]框架使用如果使用框架其CSRF防护功能是否已正确启用和配置手动渗透测试步骤信息收集使用Burp Suite等代理工具遍历目标应用的所有功能点抓取所有请求。寻找敏感操作筛选出所有非GET的请求特别是涉及用户数据修改、资金操作、权限变更的端点。检查防护对于每个敏感端点检查其请求是否有CSRF Token参数或自定义头如X-CSRF-Token如果存在Token尝试移除或修改它观察请求是否被拒绝。检查Cookie的SameSite属性可在浏览器开发者工具Application标签页查看。构造PoC对于疑似未防护或防护不当的端点使用Burp Suite的“Generate CSRF PoC”功能生成攻击测试页面。模拟攻击在一个浏览器中正常登录目标网站。在同一个浏览器的新标签页中打开生成的PoC页面。观察目标网站的操作是否被执行如密码被修改、余额变动。同时检查PoC页面的网络请求确认伪造的请求是否成功发送并携带了Cookie。测试Token绑定如果存在Token测试Token是否与用户或会话绑定。尝试将用户A的Token用于用户B的请求看是否被拒绝。测试空Referer尝试构造一个不包含Referer头的请求可通过修改PoC或使用meta namereferrer contentno-referrer观察服务器如何处理。常见绕过手法测试Token在Cookie中检查Token是否也通过Cookie下发。如果是尝试在伪造请求中只携带Cookie里的Token而不带参数/头里的Token看是否通过。这是双重Cookie验证的误用。Referer校验不严尝试篡改或删除Referer头或测试服务器是否只检查域名而忽略了路径例如信任*.example.com但evil.example.com也可能是攻击者控制的子域名。JSON CSRF对于接收JSON的API尝试将Content-Type改为text/plain或application/x-www-form-urlencoded并将JSON数据作为参数发送看后端是否仍然处理。防御CSRF不是一项一劳永逸的任务而需要贯穿于应用设计、开发、测试和运维的全生命周期。从意识上重视在架构上设计在代码中落实在测试中验证才能有效守护用户和业务的安全。在实际项目中我始终坚持“默认安全”的原则任何新的状态变更端点第一反应就是加上CSRF Token校验这已经成了肌肉记忆。同时定期使用自动化工具和手动测试进行复查确保没有遗漏。安全是一个持续的过程而对抗CSRF是我们守护Web世界大门的第一道也是至关重要的一道关卡。