1. 项目概述为什么CSRF是每个Web开发者必须跨过的坎如果你做过Web开发尤其是涉及用户登录、交易、数据修改的功能那你一定对“请求伪造”这个词不陌生。CSRF全称Cross-Site Request Forgery中文常译作“跨站请求伪造”。这名字听起来有点学术但它的本质很简单攻击者骗你的浏览器去帮你发送一个你本意并不想发送的请求。想象一下你登录了网上银行然后不小心点开了一个“有趣”的网页这个网页在后台悄悄向银行服务器发起了一个转账请求。因为你的浏览器里存着登录银行的Cookie银行服务器一看“哦是老用户发来的合法请求”于是钱就被转走了。整个过程你毫不知情这就是CSRF攻击最典型的场景。我处理过不少安全审计案例CSRF漏洞出现的频率高得惊人尤其是在一些快速迭代的业务系统中开发者往往把精力都放在功能实现和防SQL注入、XSS上却忽略了这道“来自信任客户端的偷袭”。它不直接窃取你的数据而是滥用你的身份和权限去做坏事。所以无论你是前端、后端还是全栈理解CSRF的原理、亲手复现几种攻击方式、并掌握可靠的防御策略是构建可靠Web应用的必修课。这个专题我们就从最基础的概念拆解开始一步步深入到可实操的攻击复现让你不仅知道它是什么更清楚它怎么来、怎么打、怎么防。2. CSRF攻击的核心原理与条件拆解要理解CSRF不能只停留在“冒用身份”这个层面必须深入到HTTP协议和浏览器同源策略的细节里。很多人混淆CSRF和XSS其实它们有本质区别XSS是往你的网站里“注入”恶意脚本目的是窃取信息或执行操作而CSRF是“借用”用户的浏览器向你的网站发送一个早已构造好的恶意请求。攻击者甚至不需要入侵你的服务器。2.1 攻击成功的三个必要条件一次成功的CSRF攻击必须同时满足以下三个条件缺一不可。理解这三个条件也就找到了防御的突破口关键操作存在Web应用的可预测请求目标网站存在一个通过HTTP请求通常是GET或POST就能执行的重要操作比如修改密码POST /changepassword、转账POST /transfer、发表评论POST /comment。这个请求的接口地址、参数名和格式是攻击者可以预测或探测到的。受害者已登录目标网站并保持会话受害者的浏览器必须已经通过了目标网站的认证例如包含了有效的登录会话Cookie、Authorization头等并且这个会话尚未过期。服务器正是依靠这些凭证来识别用户身份的。攻击者能诱骗受害者触发恶意请求攻击者需要以某种方式让受害者的浏览器发出那个构造好的恶意请求。最常见的方式是诱骗用户点击一个链接GET请求或访问一个嵌入了自动提交表单的恶意页面POST请求。注意这里有个关键点CSRF攻击不要求攻击者获取用户的会话凭证如Cookie。它只是“借用”了浏览器自动携带凭证的这个机制。Cookie的HttpOnly属性可以防XSS窃取Cookie但对CSRF完全无效因为浏览器发送请求时依然会乖乖带上它。2.2 CSRF与XSS的本质区别我经常用这个类比假设你的网站是一个需要刷卡Cookie进入的公司大楼服务器。XSS攻击相当于攻击者在大楼里你的网页中安放了一个伪装的内部人员恶意脚本。这个内部人员可以偷看其他员工的电脑窃取数据或者冒充你去财务室要求转账执行操作。防御的重点是不让坏人混进大楼对输入输出严格过滤。CSRF攻击相当于攻击者在大楼外伪造了一份有你签名的转账授权书恶意请求然后骗你的一个同事你的浏览器拿着这份授权书和你的门禁卡自动携带的Cookie去财务室办理了转账。防御的重点是让财务室能分辨授权书的真伪验证请求来源或添加不可预测令牌。3. 主流CSRF攻击方式实战复现与深度解析光说不练假把式。下面我们抛开理论直接进入实战环节。我会用最简单的HTML代码模拟攻击页面并假设存在一个有漏洞的目标应用。为了安全所有演示都在本地或授权的测试环境进行。你可以用DVWA、Pikachu这类靶场来亲手尝试。3.1 基于GET请求的CSRF攻击这是最简单、最古老的一种方式。很多Web早期开发中习惯用GET请求来执行操作比如a href/delete?id1删除文章/a这为CSRF大开方便之门。攻击场景假设一个博客系统删除文章的接口是GET /delete_article?article_id123。用户登录后只要访问这个URL文章就会被删除。攻击者构造的恶意页面!-- 恶意页面attacker-get.html -- !DOCTYPE html html headtitle最新搞笑图片/title/head body h1点击查看年度最佳笑话/h1 !-- 利用img标签的src属性浏览器会自动发起GET请求 -- img srchttp://vulnerable-blog.com/delete_article?article_id123 width0 height0 styledisplay:none; / p页面内容可能是一张有趣的图片或一段吸引人的文字诱导用户停留/p script // 或者使用JavaScript自动发起请求 // fetch(http://vulnerable-blog.com/delete_article?article_id123, {credentials: include}); /script /body /html攻击流程用户登录了vulnerable-blog.com会话Cookie有效。用户被诱导通过邮件、论坛等访问了attacker-get.html。页面加载时img标签会尝试加载一个“图片”其src指向的就是删除文章的URL。浏览器向vulnerable-blog.com发起一个GET请求并自动携带该域名下的Cookie。服务器收到带有合法Cookie的请求认为是用户本人操作于是执行删除。文章123在用户无感知的情况下被删除。实操心得这种攻击之所以有效是因为浏览器对img、script、link等标签的src或href属性发起的请求默认都会携带目标站点的Cookie。将图片设置为0像素并隐藏用户完全无法察觉。防御这种攻击最直接的方法就是绝对不要用GET请求执行数据写入或状态变更操作这是HTTP语义和Web开发的基本规范。3.2 基于POST请求的CSRF攻击现代Web应用普遍使用POST请求进行重要操作但这并不能阻止CSRF。攻击者可以通过一个隐藏的自动提交表单来模拟POST请求。攻击场景一个转账功能接口为POST /transfer参数为to_account和amount。攻击者构造的恶意页面!-- 恶意页面attacker-post.html -- !DOCTYPE html html headtitle抽奖活动/title/head body h2恭喜您中奖请点击下方按钮领取/h2 form idmaliciousForm actionhttp://vulnerable-bank.com/transfer methodPOST styledisplay: none; input typehidden nameto_account valueATTACKER_ACCOUNT_NUMBER / input typehidden nameamount value10000 / /form button onclickdocument.getElementById(maliciousForm).submit();领取奖金/button !-- 或者更隐蔽的方式页面加载后自动提交 -- script // 无需用户点击页面加载后自动提交表单 window.onload function() { // 可以设置一个短延时让页面先显示一下降低用户戒心 setTimeout(() { document.getElementById(maliciousForm).submit(); }, 2000); }; /script /body /html攻击流程用户登录了网上银行vulnerable-bank.com。用户访问了“抽奖活动”页面attacker-post.html。页面加载后或用户点击“领取奖金”按钮JavaScript自动提交了隐藏的表单。浏览器向银行服务器发起一个POST请求携带了转账参数和用户的会话Cookie。服务器验证Cookie有效执行转账操作。注意事项这种自动提交表单的攻击即使用户禁用了JavaScript攻击者也可以将按钮做得极具诱惑性诱骗用户手动点击。表单是HTML原生元素其提交行为不依赖JS。因此仅依赖“使用POST请求”是远远不够的。3.3 复杂场景Content-Type为JSON的POST请求攻击随着前端SPA单页应用和RESTful API的流行很多应用使用Content-Type: application/json来传输数据。传统的HTML表单无法直接发送JSON数据表单默认的Content-Type是application/x-www-form-urlencoded或multipart/form-data这曾被认为可以防御CSRF。但事实并非如此。攻击场景一个用户更新邮箱的API接口为POST /api/user/email请求体为JSON格式{newEmail: attackerevil.com}。攻击者构造的恶意页面!-- 恶意页面attacker-json.html -- !DOCTYPE html html body script // 使用Fetch API或XMLHttpRequest发送JSON请求 const maliciousData { newEmail: attackerevil.com }; // 关键设置 credentials: include 以携带Cookie fetch(http://vulnerable-api.com/api/user/email, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify(maliciousData), credentials: include // 这是关键告诉浏览器发送Cookie }).then(response { console.log(攻击请求已发送状态:, response.status); // 可以进一步诱导用户比如跳转回原站 // window.location.href http://vulnerable-api.com/settings?updated1; }); /script p页面正在加载请稍候.../p /body /html攻击流程与原理用户登录了vulnerable-api.com。访问恶意页面其中的JavaScript代码立即执行。fetch请求设置了credentials: include这指示浏览器在跨域请求中也包含Cookie等凭证遵循CORS规则但服务器如果配置不当如Access-Control-Allow-Origin: *且Access-Control-Allow-Credentials: true攻击就可能成功。浏览器发送一个携带正确Content-Type: application/json头和用户Cookie的POST请求到目标API。如果服务器端仅依赖Cookie进行身份认证且没有验证CSRF Token那么它就会处理这个请求将用户的邮箱修改为攻击者的邮箱。深度解析这里涉及到一个关键概念——简单请求与预检请求。对于application/json的请求浏览器会先发送一个OPTIONS方法的预检请求。如果服务器的CORS策略过于宽松例如Access-Control-Allow-Origin: *预检通过真正的恶意POST请求就会被发出。因此防御此类攻击绝不能依赖“JSON请求无法通过表单发起”这个过时的认知。正确的防御必须基于令牌验证或同源检查。4. 构建健壮的CSRF防御体系从理论到实践了解了攻击方式防御思路就清晰了打破CSRF攻击成立的三个必要条件中的任意一个即可。通常我们无法控制用户是否登录条件2也难以完全防止用户访问恶意链接条件3因此防御的核心聚焦在条件1让攻击者无法构造出能被服务器接受的合法请求。4.1 同步令牌模式最主流、最可靠的方案这是业界防御CSRF的黄金标准。原理是服务器在用户会话中生成一个随机、不可预测的令牌CSRF Token在渲染表单或页面时将这个令牌嵌入其中如隐藏域input typehidden namecsrf_token value...。当用户提交表单时必须将这个令牌一并提交。服务器在处理请求时会校验提交的令牌是否与会话中存储的令牌一致。服务端实现要点以Node.js/Express为例// 1. 生成并存储Token可以使用session或类似机制 const crypto require(crypto); app.use((req, res, next) { if (!req.session.csrfToken) { req.session.csrfToken crypto.randomBytes(32).toString(hex); } // 将token暴露给视图层也可以放在res.locals中 res.locals.csrfToken req.session.csrfToken; next(); }); // 2. 在渲染表单的视图中嵌入Token以EJS模板为例 // !-- form.ejs -- form action/transfer methodPOST input typehidden namecsrf_token value% csrfToken % input typetext nameto_account input typenumber nameamount button typesubmit转账/button /form // 3. 验证中间件 const validateCsrfToken (req, res, next) { const clientToken req.body.csrf_token; // 从POST body获取 const serverToken req.session.csrfToken; if (!clientToken || clientToken ! serverToken) { return res.status(403).send(CSRF Token验证失败); } // 验证通过后可以选择刷新Token每次使用后失效 req.session.csrfToken crypto.randomBytes(32).toString(hex); next(); }; app.post(/transfer, validateCsrfToken, (req, res) { // 处理安全的转账逻辑 });前端SPA如Vue/React配合API的实践 对于前后端分离的应用Token通常通过接口获取并在后续的请求中通过Header如X-CSRF-Token发送而不是表单字段。获取Token前端在初始化或登录后调用一个安全接口如GET /api/csrf-token获取Token。服务器将此Token设置在用户的Session中并返回给前端。存储与携带前端将Token存储在内存或非HttpOnly的Cookie中注意这里存在被XSS窃取的风险需结合其他防护。在发起任何非幂等的请求POST, PUT, DELETE等时将其放入请求头。// 前端请求示例使用axios import axios from axios; // 假设从某个接口或Cookie中获取了token let csrfToken getCsrfTokenFromStorage(); axios.post(/api/transfer, data, { headers: { X-CSRF-Token: csrfToken }, withCredentials: true // 如果需要发送Cookie });服务端验证服务器端中间件检查X-CSRF-Token头的值是否与Session中的值匹配。实操心得与避坑指南Token的强度必须使用密码学安全的随机数生成器如crypto.randomBytes长度足够至少32字节防止被爆破。Token的绑定Token应与用户会话Session严格绑定。最好还能与具体的操作或表单ID绑定提供双重保障。Token的时效性可以考虑每次验证后使旧Token失效生成新Token同步令牌模式。这能有效防止重放攻击但要注意避免多标签页操作导致的Token失效问题。另一种方案是使用时效较长的Token。GET请求的豁免通常不对GET、HEAD、OPTIONS等幂等请求进行CSRF校验因为它们不应该改变服务器状态。但务必确保你的应用严格遵守HTTP语义。SPA的安全考量对于SPA将CSRF Token放在Cookie中而非LocalStorage并由前端JavaScript读取后放入Header是一种常见模式。但这要求你妥善防护XSS因为XSS可以读取该Cookie。一个更安全的变种是“Double Submit Cookie”模式。4.2 双重Cookie提交模式这种模式是同步令牌模式的一个变体更适合纯API场景。原理是服务器在用户登录后设置一个随机值的Cookie例如csrf_tokenabc123这个Cookie的HttpOnly属性可以设为false以便前端JS读取。前端JavaScript从Cookie中读取这个值。前端在发起请求时除了浏览器会自动携带该Cookie外还需要手动将这个值添加到一个自定义的Header里如X-CSRF-Token。服务器收到请求后比较请求头X-CSRF-Token中的值和Cookie中的csrf_token值是否一致。为什么这样能防御CSRF攻击者构造的恶意页面可以诱使浏览器发送请求并携带Cookie因为Cookie是浏览器自动加的但他无法让恶意页面中的JavaScript读取到目标站点的Cookie受同源策略保护因此也就无法构造出正确的X-CSRF-Token请求头。服务器校验失败请求被拒绝。实现示例// 服务端设置Cookie app.post(/login, (req, res) { // ... 验证逻辑 const csrfToken crypto.randomBytes(32).toString(hex); res.cookie(csrf_token, csrfToken, { httpOnly: false, // 允许JS读取 sameSite: Lax, // 推荐设置 // secure: true // 生产环境应启用 }); // ... 返回登录成功 }); // 服务端验证中间件 const validateDoubleCookie (req, res, next) { const tokenFromHeader req.headers[x-csrf-token]; const tokenFromCookie req.cookies.csrf_token; if (!tokenFromHeader || tokenFromHeader ! tokenFromCookie) { return res.status(403).send(CSRF验证失败); } next(); }; app.post(/api/action, validateDoubleCookie, handler);注意事项此方法的安全性依赖于同源策略对Cookie读取的限制。如果网站存在XSS漏洞攻击者可以注入脚本读取Cookie从而获取Token那么此防御即告失效。因此防御CSRF的前提是已经做好了XSS的防护。同时务必设置Cookie的SameSite属性见下文。4.3 利用SameSite Cookie属性SameSite是Cookie的一个属性用于控制Cookie在跨站请求时是否被发送。它有三个值Strict最严格。Cookie仅在同站请求即当前网页的URL与请求目标一致时发送。这意味着从其他网站链接过来的请求即使是GET请求也不会携带Cookie。这能有效防御所有CSRF但可能影响用户体验例如从邮件链接点击进入网站需要重新登录。Lax默认值现代浏览器的默认行为一种平衡方案。在跨站的顶级导航如点击链接且是安全的HTTP方法如GET时会发送Cookie。但对于跨站的POST请求、iframe加载、AJAX请求等则不发送Cookie。这能防御大多数由form提交和fetch/XMLHttpRequest发起的CSRF攻击同时保持了基本的用户体验。NoneCookie在所有上下文中都会发送但必须同时设置Secure属性即仅限HTTPS。设置方法// 在设置会话Cookie或认证Cookie时 res.cookie(sessionId, value, { httpOnly: true, secure: true, // 生产环境必须 sameSite: Lax // 或 Strict });实战意义将关键的身份认证Cookie设置为SameSiteLax或Strict是当前防御CSRF最简单、最有效的一线方案之一。它直接从浏览器层面切断了大多数跨站请求携带认证凭证的可能性。你应该在所有项目中都主动设置此属性。4.4 验证请求来源Referer与Origin Header服务器可以检查HTTP请求头中的Referer或更标准的Origin字段来判断请求是否来自同一个站点。Referer表示请求是从哪个页面链接过来的。但可能被篡改或缺失例如用户直接从地址栏输入或浏览器隐私设置。Origin用于CORS请求表示请求的发起来源。对于同源请求浏览器通常不发送Origin头对于跨域请求则会发送且不可由用户自定义比Referer更可靠。防御逻辑const checkOrigin (req, res, next) { const origin req.get(Origin) || req.get(Referer); const trustedOrigins [https://your-trusted-site.com, https://your-app.com]; // 允许的源列表 if (req.method POST || req.method PUT || req.method DELETE) { if (origin) { const originHostname new URL(origin).hostname; if (!trustedOrigins.includes(originHostname)) { return res.status(403).send(非法请求来源); } } else { // 对于没有Origin/Referer的敏感请求可以采取更严格的措施比如要求Token // return res.status(403).send(请求来源不明); } } next(); };重要提醒单独依赖来源检查是不安全的。因为Referer头可能被某些浏览器插件、防火墙或用户配置移除导致合法请求被误拦。更严重的是在某些网络环境下或通过某些技术手段如利用Flash等历史漏洞攻击者有可能篡改或伪造Referer头。因此来源检查应作为辅助防御手段与CSRF Token等主方案结合使用。5. 实战中常见问题排查与高级对抗技巧在实际开发和渗透测试中你会遇到各种边界情况和复杂的应用架构。这里分享一些我踩过的坑和应对技巧。5.1 多标签页/单页应用下的Token管理问题用户同时打开两个标签页操作同一应用。标签页A提交表单后服务器使旧的CSRF Token失效并生成新的。此时用户切换到标签页B仍持有旧Token提交表单会导致Token验证失败用户体验差。解决方案方案AToken不立即失效。为Token设置一个较长的有效期如整个会话期间或使用加密签名Token如JWT格式服务端只需验证签名有效性无需在Session中存储比对。但这会略微降低安全性Token泄露后的窗口期变长。方案B使用“每表单Token”。为每个表单生成独立的Token并记录其有效性。一个表单提交只使该表单的Token失效不影响其他页面。方案C前端感知Token更新。对于SPA可以在每次非GET请求的响应中由服务器返回一个新的CSRF Token前端自动更新存储。这需要前后端协作。5.2 文件上传功能的CSRF防护问题文件上传通常是multipart/form-data格式的表单。传统的在请求体body中插入CSRF Token字段的方式完全有效。但需要注意验证逻辑要在处理文件流之前进行避免消耗服务器资源。中间件顺序示例const multer require(multer); const upload multer({ dest: uploads/ }); app.post(/upload, validateCsrfToken, // 1. 先验证CSRF Token从urlencoded字段中读取 upload.single(file), // 2. 再处理文件上传 (req, res) { // 3. 处理业务逻辑 } );5.3 第三方插件/跨域资源引入的风险问题如果你的网站允许用户嵌入来自第三方域的脚本、iframe或通过script标签加载JSONP接口这些第三方资源如果被攻破就可能成为CSRF攻击的跳板。防御策略严格CORS策略对API接口不要设置Access-Control-Allow-Origin: *而是精确指定可信的源。对于需要凭证的请求Access-Control-Allow-Origin不能为通配符*必须指定具体域名。慎用JSONPJSONP由于其通过script标签加载的特性天生不受同源策略限制极易引发CSRF。在现代开发中应优先使用CORS替代JSONP。内容安全策略部署CSP限制脚本、表单提交等操作的源可以显著增加攻击难度。5.4 自动化测试与漏洞扫描中的CSRF检测在安全测试中如何快速检测CSRF漏洞手动测试抓取一个关键操作如修改密码的请求包。移除请求中的CSRF Token、Referer、Origin等疑似防护字段。重放请求观察是否成功。如果成功则存在漏洞。尝试用另一个已登录用户的会话Cookie重放该请求去掉Token等如果也成功则漏洞危害更大。工具辅助使用Burp Suite的“Engagement tools” - “Generate CSRF PoC”功能可以自动生成测试用的恶意HTML页面。使用OWASP ZAP等扫描器其主动扫描规则中包含CSRF检测。代码审计检查关键POST/PUT/DELETE路由是否配备了CSRF防护中间件。检查防护中间件的实现是否正确如Token是否随机、是否绑定会话、是否校验。检查是否有路由被意外豁免了防护。6. 从防御到架构将安全思维融入开发流程真正的安全不是靠最后一刻的修补而是融入开发和设计的每一个环节。对于CSRF防护我建议团队遵循以下实践框架优先使用成熟的、有良好安全记录的开发框架如Spring Security、Django、Laravel、Express csurf中间件等。这些框架通常内置了开箱即用的CSRF防护并经过了广泛测试。安全中间件标准化在项目初始化时就配置并启用全局的CSRF防护中间件。将其作为所有非GET路由的默认拦截器只有明确豁免的路由才需要额外配置。同源策略与CORS审慎配置明确区分对内API和对外API。对内API服务于自己的前端应严格限制Origin。对外API开放给第三方则需要精细的CORS策略、API密钥、OAuth等认证授权机制不能仅依赖Cookie。强制使用HTTPSSameSiteCookie的None值、SecureCookie属性等都依赖HTTPS。全站HTTPS是现代Web安全的基础。定期安全培训与代码审查让团队成员都理解CSRF的原理和危害。在代码审查中将安全防护包括CSRF Token检查、CORS头设置等作为必审项。自动化安全测试将CSRF漏洞扫描纳入CI/CD流水线使用静态应用安全测试和动态应用安全测试工具进行常态化检测。CSRF是一个经典的Web安全问题它巧妙地利用了Web最基础的信任机制——浏览器的同源策略和自动凭证管理。防御它的技术并不复杂核心思想就是增加一个攻击者无法预测、无法伪造的“挑战码”。无论是同步令牌、双重Cookie还是SameSite属性都是这一思想的具体实现。作为开发者我们需要做的是第一深刻理解其原理第二在项目中系统性地实施至少一种主防御方案推荐Token SameSiteLax第三通过安全编码和架构设计将这种防御变为一种习惯。安全是一个持续的过程保持警惕持续学习才能构建出真正让用户放心的应用。