1. 从一次“被转账”说起理解CSRF攻击的本质几年前我在负责一个电商项目的安全审计时遇到了一个非常典型的案例。一位用户反馈他登录账户后只是正常浏览了几个商品页面第二天就发现账户里多出了一笔他本人从未操作过的“代金券赠送”记录赠送对象是一个陌生的账号。用户坚称自己没有进行过此操作而我们的后台日志却清晰地显示该请求确实是从他登录的会话中发出的IP地址也吻合。这听起来像是一个“内部作案”或者账号被盗但经过深入排查我们发现问题的根源并非密码泄露而是一种被称为“跨站请求伪造”的攻击也就是我们今天要深入探讨的CSRF。简单来说CSRF攻击就是攻击者诱导已经登录了目标网站比如银行、电商、社交平台的用户在用户不知情的情况下向该网站发送一个恶意请求。因为浏览器会自动携带用户的登录凭证如Cookie所以服务器会认为这是用户本人的合法操作从而执行攻击者预设的动作比如转账、修改密码、发表评论或者像我们案例中那样赠送资产。为什么这个攻击如此危险且隐蔽核心在于它利用了Web应用最基础的信任机制浏览器对Cookie的自动发送机制。当你登录一个网站后服务器会给你一个会话Cookie之后的每一次请求浏览器都会乖乖地、自动地把这个Cookie附加上去以证明“你是你”。CSRF攻击者不偷你的Cookie而是“借用”你的Cookie让你在浑然不觉中替他办事。这就像有人拿到了你盖好章的空白支票他不需要知道你的保险柜密码偷Cookie只需要诱导你把支票用在错误的地方。从最近的热搜词如“74cms靶场csrf”、“pikachu csrf”、“dvwa靶场通关教程csrf”可以看出CSRF是Web安全学习与实践中的经典课题和常客。而“本网站使用安全服务防护恶意自动程序”这类提示虽然主要针对自动化爬虫或CC攻击但其背后的安全防护思想——验证请求的合法性——与防御CSRF是相通的。理解CSRF不仅是安全工程师的必修课也是每一位Web开发者在构建系统时必须绷紧的一根弦。2. CSRF攻击的运作原理与常见场景拆解要有效防护必须先透彻理解攻击是如何发生的。CSRF攻击的成功需要同时满足几个关键条件我们可以把它想象成一个精心设计的“骗局”。2.1 攻击发生的三个必要条件用户已登录并持有有效会话这是前提。用户必须在目标网站例如bank.com处于登录状态浏览器中保存了该站点的认证Cookie。目标站点存在可预测的操作端点攻击者需要知道一个能执行敏感操作的URL及其参数。例如银行转账的接口可能是POST https://bank.com/transfer参数是to_account和amount。这个接口缺乏对请求来源的验证。用户被诱导访问恶意页面攻击者通过邮件、论坛、社交媒体等渠道发布一个包含恶意代码的链接或页面诱使用户点击或访问。2.2 两种主流的攻击载荷Payload方式攻击者构造恶意请求的方式主要有两种对应HTTP的GET和POST方法。GET型CSRF最简单直接这种方式通常用于那些用GET请求就能触发的敏感操作这本身是糟糕的设计。攻击者只需要构造一个包含参数的URL并诱使用户访问。假设银行有一个危险的转账接口GET https://bank.com/transfer?toattackeramount10000攻击者可以在论坛发一张图片img srchttps://bank.com/transfer?toattackeramount10000 width0 height0 /当已登录银行的用户浏览这个帖子时浏览器会尝试加载这张“图片”自动向银行发送带有用户Cookie的转账请求。因为图片加载失败通常没有明显提示用户完全感知不到。注意在实际开发中绝对不要用GET方法来实现任何会产生副作用的操作如修改数据、支付、注销。这是防御CSRF的第一道也是最重要的设计准则。POST型CSRF更为隐蔽现代Web应用普遍使用POST进行敏感操作但这并不能免疫CSRF。攻击者可以构建一个隐藏的HTML表单并通过JavaScript自动提交。假设转账接口是POST到https://bank.com/transfer。 攻击者构造的恶意页面代码如下body onloaddocument.forms[0].submit() form actionhttps://bank.com/transfer methodPOST input typehidden nameto_account valueATTACKER_ACCOUNT / input typehidden nameamount value5000 / !-- 可能还有其他必需的隐藏参数 -- /form /body攻击者诱使用户访问这个页面。页面加载 (onload) 后表单会自动提交。由于用户浏览器里有银行的登录Cookie这个POST请求会被银行服务器认为是用户自愿发起的。从热词“csrfpost”可以看出POST型CSRF是学习和测试的重点。像Pikachu、DVWA这些靶场都提供了POST型CSRF的实战场景帮助开发者理解其原理。2.3 典型攻击场景还原理解了原理我们来看几个更贴近生活的场景场景一社交工程与钓鱼邮件。你收到一封标题为“看看我们上次聚会的照片”的邮件里面有一个链接。点击后页面一闪而过可能显示“图片加载失败”。但与此同时你可能已经在另一个标签页登录的社交网站上自动关注了某个账号或者转发了某条广告。因为那个链接实际上是一个精心构造的CSRF攻击URL。场景二恶意广告与评论链接。在论坛或博客的评论区攻击者留下一个“这个漏洞修复方案请看这里”的链接指向一个恶意页面。好奇的管理员点击后可能就在后台执行了添加攻击者为超级管理员的操作。场景三结合XSS的进阶攻击。如果网站本身还存在跨站脚本XSS漏洞攻击者可以注入脚本从内部发起更复杂的CSRF攻击甚至绕过一些简单的防御措施。这也是为什么安全防御需要多层次、纵深化的原因。实操心得在安全测试中不要只盯着“修改密码”、“转账”这种高危功能。像“修改邮箱”、“更改收货地址”、“发表评论”、“投票点赞”这类中低频操作同样是CSRF的重灾区因为它们容易被开发者忽视却足以对用户造成骚扰或损失。3. 构建防线CSRF攻击的防护策略详解知道了攻击怎么来我们就能有针对性地筑墙。CSRF防护的核心思想是让服务器有能力区分“用户自愿发出的请求”和“攻击者伪造的请求”。以下是经过实战检验的几种主流防护方案。3.1 同源策略与验证HTTP Referer/Origin头部这是最直观的一种思路。HTTP请求头中的Referer或更现代的Origin字段表明了请求是从哪个页面发起的。原理服务器检查请求的Referer或Origin值如果它不是来自本网站合法的域名则拒绝请求。如何操作在服务器端拦截所有状态变更的请求POST, PUT, DELETE等。提取请求头中的Referer或Origin字段。判断其是否来源于受信任的域名如你自己的网站域名。对于Origin检查是否与当前服务的域名匹配对于Referer检查其域名部分。优点实现相对简单零客户端成本。缺点与注意事项隐私与兼容性有些用户浏览器出于隐私考虑会禁用发送Referer。一些浏览器插件或网络代理也可能剥离或修改该头部。依赖它可能导致合法用户请求被拒绝。绕过风险历史上存在一些方法可以篡改或伪造Referer头尽管在现代浏览器中难度增加。此外如果网站本身存在开放重定向漏洞攻击者可能利用该漏洞使Referer看起来“合法”。判断逻辑要严谨验证时一定要使用白名单机制并精确匹配协议、域名和端口https://your-site.com:443。简单的字符串包含检查如indexOf(‘your-site.com’)可能被attacker-your-site.com这样的域名绕过。提示Origin头部在现代浏览器中用于跨域请求如CORS时更为可靠且不会包含完整的路径信息隐私性稍好。可以将Origin作为主要验证手段Referer作为备用。但这只能作为辅助防御措施不应作为唯一依赖。3.2 使用Anti-CSRF Token同步器令牌模式这是目前公认最有效、最主流的CSRF防护方案被OWASP强烈推荐。其原理是“一次一密”。核心原理用户访问包含表单的页面时如转账页面服务器生成一个随机、不可预测的令牌Token将其存放在两个地方一是服务器的会话Session中二是作为隐藏字段嵌入到返回给用户的HTML表单里。用户提交表单时这个隐藏的Token会随着表单数据一起提交到服务器。服务器收到请求后比较请求体中的Token和会话中存储的Token是否一致。一致则认为是合法请求否则拒绝。实操步骤与细节Token生成使用密码学安全的随机数生成器如Java的java.security.SecureRandomPython的os.urandom或secrets.token_urlsafe生成足够长度建议16字节以上的随机字符串。Token存储将Token以Key-Value形式存储在服务器端的用户会话Session中。Key可以是固定的如_csrf_tokenValue就是生成的随机字符串。Token下发在渲染表单页面时将Token作为一个隐藏的input字段输出。form action/transfer methodPOST input typehidden name_csrf_token value生成的随机Token值 !-- 其他表单字段 -- input typetext nameto_account input typenumber nameamount button typesubmit转账/button /formToken验证在服务器端处理POST请求的入口处从请求参数中获取_csrf_token同时从当前用户会话中读取存储的Token进行字符串比较。验证成功后务必使当前会话中的Token立即失效删除或标记为已使用防止令牌被重复使用重放攻击。关键难点与解决方案多标签页/异步操作一个用户同时打开多个表单页每个页面都需要独立的Token。解决方案是为每个表单生成独立的Token或采用“每个会话一个主Token但每次验证后更新”的策略。对于单页应用SPA的异步请求AJAX可以将Token放在HTTP请求头中如X-CSRF-TOKEN并在每次重要操作前从后端获取新的Token。性能考量对于超高并发场景Session存储可能成为瓶颈。可以考虑将Token加密后直接下发给客户端并在验证时解密和校验其有效性如包含时间戳和签名减少对中心化Session存储的依赖。但这会引入加解密的开销需要权衡。Token泄露如果网站同时存在XSS漏洞攻击者可以通过JavaScript窃取页面中的Token从而使CSRF防护失效。因此防御CSRF必须与防御XSS结合安全是一个整体。实操心得在实现Token验证中间件时不要只保护“看起来危险”的操作。最好采用“默认拒绝”策略为所有非幂等的HTTP方法POST, PUT, PATCH, DELETE全局启用CSRF Token检查仅对只读操作GET, HEAD, OPTIONS放行。Spring Security、Django、Laravel等主流框架都提供了开箱即用的CSRF防护中间件优先使用它们比自己从头实现更可靠。3.3 双重Cookie验证这是一种利用浏览器同源策略限制的巧妙方法在特定场景下可以作为补充。原理用户访问网站时后端除了设置会话Cookie如SessionID再额外设置一个随机Token到另一个Cookie中如csrf_token。前端JavaScript代码必须在同源下才能可靠读取Cookie读取这个csrf_tokenCookie的值。在发起敏感请求如AJAX时前端手动将这个Token值作为请求参数或自定义头部如X-CSRF-TOKEN附加到请求中。后端同时验证请求中的Token值和Cookie中的Token值是否一致。优点实现简单前端参与度可控。致命缺点Cookie可能被攻击者设置如果网站存在子域名泛解析*.your-domain.com攻击者在attacker.your-domain.com上可以设置父域名your-domain.com的Cookie从而污染用户的Cookie空间伪造出合法的Token。因此必须确保Cookie被设置为SameSiteStrict或Lax并且使用精确的域名。依赖前端JavaScript如果前端代码被XSS攻破此方案完全失效。同时对于非AJAX的传统表单提交需要额外逻辑来嵌入Token。 由于这些缺陷双重Cookie验证通常不作为首选的独立方案但可以与其他方案如SameSite Cookie结合使用。3.4 利用SameSite Cookie属性现代浏览器的利器这是近年来最有效的“普惠式”CSRF缓解措施直接从浏览器机制层面入手。原理SameSite是Cookie的一个属性用于控制Cookie在跨站请求时是否被发送。它有三个值Strict最严格。Cookie仅在同站请求即当前页面的URL与请求目标URL的“站点”相同时发送。这意味着从其他网站链接过来时用户处于未登录状态。Lax默认值宽松模式。在跨站的顶级导航如点击链接且是安全GET方法时发送Cookie。这对于用户体验友好从搜索引擎结果点击进来可以保持登录同时阻止了大多数CSRF攻击因为CSRF通常通过formPOST或imgGET发起这些都不是顶级导航。NoneCookie在所有上下文中都会发送但必须同时设置Secure属性仅限HTTPS。如何操作在设置会话Cookie时直接指定SameSite属性。Set-Cookie: SessionIdabc123; Path/; HttpOnly; Secure; SameSiteLax巨大优势几乎零成本部署。只需修改服务器设置Cookie的代码无需改动业务逻辑。现代浏览器Chrome, Firefox, Safari, Edge新版本均已默认将未指定SameSite的Cookie视为Lax这为整个Web生态提供了基础的CSRF防护。注意事项兼容性需要确保你的用户主要使用现代浏览器。对于仍需支持老旧浏览器的关键业务需要降级方案。不影响第三方集成如果你的网站需要被其他网站通过iframe嵌入或作为第三方API服务SameSiteLax/Strict可能会阻止必要的Cookie发送需要仔细评估并可能对特定Cookie使用SameSiteNone; Secure。不是银弹SameSiteLax无法防御所有GET型CSRF如果网站错误地用GET执行写操作也无法防御同站点的攻击即网站本身存在XSS漏洞。它必须与其他防护措施如Token结合使用。4. 实战部署从开发到上线的防护体系搭建理论需要落地。下面我将以一个典型的Web应用假设使用Java Spring Boot后端和Thymeleaf模板引擎为例详细说明如何从零开始部署一套以Token为核心的CSRF防护体系并融入其他防御层。4.1 后端防护中间件实现我们首先在后端实现一个全局的CSRF Token生成与验证过滤器。// 示例一个简单的CSRF Token工具类及过滤器 (Java) import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.security.SecureRandom; import java.util.Base64; Component public class CsrfTokenFilter implements Filter { private static final String CSRF_TOKEN_NAME _csrf; private static final SecureRandom secureRandom new SecureRandom(); Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; HttpServletResponse httpResponse (HttpServletResponse) response; HttpSession session httpRequest.getSession(false); // 1. 对于需要保护的请求方法进行验证 if (POST.equalsIgnoreCase(httpRequest.getMethod()) || PUT.equalsIgnoreCase(httpRequest.getMethod()) || PATCH.equalsIgnoreCase(httpRequest.getMethod()) || DELETE.equalsIgnoreCase(httpRequest.getMethod())) { if (session ! null) { String sessionToken (String) session.getAttribute(CSRF_TOKEN_NAME); String requestToken httpRequest.getParameter(CSRF_TOKEN_NAME); // 2. 验证Token是否存在且匹配 if (sessionToken null || !sessionToken.equals(requestToken)) { httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, Invalid or missing CSRF token.); return; // 验证失败中断请求 } // 3. 验证成功后使旧Token失效可选但推荐 session.removeAttribute(CSRF_TOKEN_NAME); } } // 4. 如果是GET请求或验证通过继续处理并为后续请求生成新Token if (session ! null httpRequest.getMethod().equalsIgnoreCase(GET)) { // 生成新的Token并存入Session String newToken generateToken(); session.setAttribute(CSRF_TOKEN_NAME, newToken); // 将Token放入请求属性供视图层使用 httpRequest.setAttribute(CSRF_TOKEN_NAME, newToken); } chain.doFilter(request, response); } private String generateToken() { byte[] bytes new byte[16]; // 128位 secureRandom.nextBytes(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); } // init 和 destroy 方法省略... }关键点解析Token生成使用SecureRandom确保随机性Base64编码后便于在HTML和HTTP中传输。验证时机仅对状态变更方法POST, PUT, PATCH, DELETE进行强制验证。GET、HEAD等幂等方法通常不验证但前提是它们不执行写操作。Token失效验证成功后立即从Session中移除Token确保其一次性使用。这能有效防御重放攻击。Token下发在GET请求处理完毕后生成新Token并放入Session和请求属性以便在渲染页面时嵌入表单。4.2 前端表单与异步请求集成后端准备好了前端需要配合嵌入和携带Token。传统服务端渲染表单Thymeleaf示例!-- 在模板中直接使用后端传递的Token -- form action/transfer methodpost th:action{/transfer} !-- CSRF Token 隐藏域 -- input typehidden th:name${_csrf.parameterName} th:value${_csrf.token} / !-- 其他表单字段 -- input typetext nametoAccount required / input typenumber nameamount required / button typesubmit确认转账/button /form使用Spring Security等框架时_csrf对象会自动注入模型。单页应用SPA与AJAX请求 对于前端框架如React, VueToken通常通过后端API的一个端点获取然后存储在内存或非HttpOnly的Cookie中并在每次请求时附加。获取Token应用初始化时调用GET /api/csrf-token接口后端返回一个Token。存储与携带前端将其保存在内存如Vuex/Redux或一个专门的Cookie中不能是HttpOnly因为JS需要读取。对于Axios等HTTP客户端可以配置请求拦截器自动附加Token。// Axios 请求拦截器示例 import axios from axios; let csrfToken ; // 从接口初始化 axios.interceptors.request.use(config { if (csrfToken [post, put, patch, delete].includes(config.method.toLowerCase())) { // 作为请求头携带是更推荐的方式避免Token出现在URL或日志中 config.headers[X-CSRF-TOKEN] csrfToken; // 或者作为参数config.params { ...config.params, _csrf: csrfToken }; } return config; });Token刷新每次提交敏感请求后后端应返回一个新的Token前端需要更新存储。或者后端可以在Token验证失败时返回特定状态码如419前端捕获后重新获取Token并重试请求。4.3 部署配置与安全加固设置SameSite Cookie在应用服务器或反向代理如Nginx中为会话Cookie强制添加SameSiteLax; Secure属性。这是最重要的基础防护。# Nginx 配置示例 (在location或server块中) proxy_cookie_path / /; secure; HttpOnly; SameSiteLax;关键操作二次确认对于转账、修改密码、删除账户等极高危操作除了CSRF防护必须在业务逻辑层增加二次确认。例如要求用户输入当前密码、使用短信/邮箱验证码、或在一个独立的确认页面再次提交。这增加了攻击者的难度即使CSRF防护存在未知缺陷这也是一道有效的业务逻辑屏障。验证HTTP Referer/Origin头作为补充在CSRF过滤器中可以增加对Origin或Referer头的检查作为深度防御的一环。但逻辑要严谨允许为空考虑到隐私设置或为受信任的源。String origin httpRequest.getHeader(Origin); String referer httpRequest.getHeader(Referer); // 简单的白名单检查示例 if (origin ! null !origin.startsWith(https://your-trusted-domain.com)) { // 拒绝请求 } // 注意此检查应作为辅助不能替代Token验证。监控与日志记录所有CSRF Token验证失败的请求包括IP、User-Agent、请求路径和时间。异常的集中失败可能是攻击探测的迹象。但注意不要记录Token值本身。5. 疑难排查与进阶攻防思考即使部署了防护在实际运行中仍可能遇到各种问题。以下是一些常见场景的排查思路和进阶安全考量。5.1 常见问题速查表问题现象可能原因排查步骤与解决方案用户提交表单总是提示“无效CSRF Token”1. Token未正确嵌入表单。2. Session丢失或不匹配。3. 浏览器禁用了Cookie。1. 检查页面HTML源码确认隐藏的input字段存在且值非空。2. 确认服务器Session配置正确且请求间Session ID保持一致检查Cookie。3. 引导用户启用Cookie或考虑将Token也放入LocalStorage并通过JS读取需防范XSS。AJAX请求携带了Token但仍被拒绝1. Token未放在后端期望的位置参数 vs 头部。2. 跨域请求未正确配置CORS。1. 使用浏览器开发者工具的“网络”选项卡检查请求负载或头部确认Token已发送且名称正确。2. 如果是跨域请求确保后端CORS配置允许该源并且可能需要在响应头中暴露自定义头部如Access-Control-Expose-Headers。多标签页操作后提交的请求失败Token被重复使用或过早失效。检查Token验证逻辑是“一次一用”还是“每会话一个”。如果是一次一用第二个标签页提交时第一个已使Token失效。解决方案可以为每个表单生成唯一Token如将表单ID与Token关联或采用“每请求生成新Token”的策略并在验证时允许最近生成的N个Token之一有效。集成第三方支付/登录回调时失败回调是跨站POST请求被CSRF防护拦截。对于明确可信的第三方回调URL可以在CSRF防护中间件中配置白名单绕过对这些特定路径的检查。务必谨慎白名单范围要尽可能小并确保回调接口自身有其他验证机制如签名。手机App或桌面客户端调用API失败非浏览器环境没有Cookie和Session概念。为API设计独立的认证方案如使用OAuth 2.0、JWTJSON Web Token等。CSRF防护主要针对基于Cookie/Session的浏览器应用。对于使用Token认证的APICSRF风险天然较低但需防范Token泄露。5.2 进阶威胁与防御思考Cookie Tossing / 投掷攻击攻击者如果能在你的域名下设置一个Cookie例如通过子域名漏洞或未净化的用户输入设置Cookie名他可能会设置一个与你CSRF Token同名的Cookie。如果后端错误地从Cookie中读取Token进行验证而不是从Session就可能被绕过。防御始终坚持从服务器端Session中读取基准Token请求中的Token只从参数或头部获取。结合XSS的绕过这是最危险的场景。如果网站存在XSS漏洞攻击者注入的脚本可以轻松读取页面中的CSRF Token然后构造一个合法的伪造请求。防御CSRF Token不能防御XSS。必须通过严格的输入输出编码、内容安全策略CSP等手段彻底杜绝XSS漏洞。安全是一个链条最薄弱的一环决定整体强度。登录态下的CSRFCSRF通常针对已登录用户。但攻击也可以针对登录接口本身。如果用户在攻击者网站操作时自动向你的网站提交登录请求并成功那么用户后续就在攻击者不知情的情况下以自己身份登录了你的网站。虽然这不会直接导致数据泄露但可能用于制造虚假活动或结合其他漏洞。防御登录请求也应受到CSRF保护或者在登录时要求进行额外的验证如图片验证码。使用Content Security Policy (CSP)CSP HTTP头可以限制页面可以加载哪些外部资源脚本、样式、图片等。通过禁止内联脚本和限制脚本来源可以极大增加攻击者实施CSRF尤其是需要复杂JS配合时和XSS的难度。这是一个重要的深度防御措施。Content-Security-Policy: default-src self; script-src self https://trusted.cdn.com;个人体会在安全防护上我始终坚持“纵深防御”和“不信任任何客户端输入”的原则。CSRF Token是核心但绝不能是唯一。SameSite Cookie提供了底层保障关键操作二次确认增加了业务逻辑门槛严格的CSP和XSS防护消除了更大的攻击面。定期使用74cms、pikachu这类靶场进行自测或者使用Burp Suite、OWASP ZAP等工具进行自动化扫描能帮助你持续发现防护体系中的盲点。安全没有终点它是一个需要持续关注、学习和加固的过程。