CSRF防护实战:从Referer校验到Token机制的安全部署

📅 发布时间:2026/7/9 4:31:18
CSRF防护实战:从Referer校验到Token机制的安全部署 1. 项目概述为什么CSRF依然是悬在头顶的剑做Web开发或者安全测试有些年头了CSRF跨站请求伪造这个老话题几乎每次做安全审计都会遇到。表面上看现在框架都内置了防护比如Spring Security、Django的CSRF中间件好像开箱即用万事大吉。但实际情况是很多团队要么对原理一知半解配置不当要么过度依赖单一机制比如只校验Referer结果被轻易绕过。最近在复盘几个内部项目的安全漏洞时发现由CSRF防护不当引发的问题依然占了不少比例尤其是那些自研的、或者对第三方组件做了深度定制的系统。CSRF攻击的本质是攻击者诱骗已经登录了目标网站的用户去访问一个恶意构造的页面从而以该用户的身份执行非本意的操作。比如用户登录了网银然后不小心点了一个“抽奖”链接这个链接背后可能就隐藏着一个向攻击者账户转账的请求。因为浏览器会自动带上用户的会话Cookie服务器无法区分这个请求是用户自愿发出的还是被伪造的。所以防护的核心思路就是让服务器有能力判断这个请求是否来源于它信任的页面。目前主流的两种防护思路——Referer校验和CSRF Token——就是基于这个逻辑诞生的。但就像任何安全机制一样如果理解不透彻、实现不严谨它们本身也会成为新的攻击面。这篇文章我就结合实战中的案例把这两种防护机制的里里外外、攻防对抗的细节掰开揉碎了讲清楚。2. Referer校验一道容易被绕过的“前门”很多应用会把HTTP请求中的Referer头作为判断请求来源的依据。这个想法很直观如果一个修改密码的POST请求其Referer显示来自attacker.com而不是本应用自己的域名那显然有问题应该拒绝。但正是这种直观容易让人放松警惕。2.1 Referer校验的工作原理与常见实现误区Referer注意在HTTP标准里这个单词就拼错了应该是Referrer但将错就错沿用至今是一个可选的HTTP请求头它表示当前请求是从哪个页面链接过来的。浏览器在用户点击链接或提交表单时通常会但不是绝对自动添加这个头。一个简单的、有缺陷的校验代码可能长这样以Python Flask为例from flask import request, abort app.route(/change_email, methods[POST]) def change_email(): # 有缺陷的Referer校验逻辑 referer request.headers.get(Referer) if referer: # 错误示例1仅检查是否包含自己的域名 if my-secure-app.com not in referer: abort(403, Invalid request origin.) # 如果Referer头不存在则跳过检查 # ... 执行更改邮箱的逻辑 ...这段代码有两个致命问题存在性依赖只在Referer头存在时才校验。攻击者可以通过技术手段让浏览器不发送Referer头。校验逻辑脆弱使用in操作符检查域名是否包含。这为绕过留下了巨大空间。2.2 实战中的Referer校验绕过手法在实际的渗透测试中绕过这类脆弱的Referer校验是家常便饭。下面我列举几种最常见且有效的手法。2.2.1 利用meta标签移除Referer这是最直接的方法。如果应用只在Referer存在时校验那么让浏览器不发这个头就行了。攻击者可以在托管CSRF攻击的HTML页面中加入以下标签!DOCTYPE html html head meta namereferrer contentno-referrer !-- 或 contentnever, no-referrer-when-downgrade 等具体策略影响不同 -- /head body form actionhttps://victim-app.com/change_email methodPOST input typehidden nameemail valueattackerevil.com input typesubmit value点击抽奖 /form scriptdocument.forms[0].submit();/script /body /htmlmeta namereferrer contentno-referrer这行代码指示浏览器在从该页面发起任何请求时都不携带Referer头。于是受害者访问这个页面并自动提交表单时发往victim-app.com的请求中将没有Referer头从而绕过“存在才校验”的逻辑。注意现代浏览器对Referrer策略的支持非常细致no-referrer是最严格的。在构造POC概念验证时需要根据目标用户的浏览器环境进行测试。2.2.2 利用子域名或路径进行匹配绕过如果应用的校验逻辑是“检查Referer是否以https://victim-app.com开头”攻击者可以注册一个像victim-app.com.attacker.net这样的域名。当请求从这个域名下的页面发出时Referer可能是https://victim-app.com.attacker.net/csrf.html它确实以https://victim-app.com开头从而通过校验。更常见的是“检查Referer是否包含victim-app.com”这种逻辑。攻击者可以将目标域名放在URL的查询参数query string或片段标识符fragment中https://attacker.net/csrf?victim-app.comhttps://attacker.net/csrf#victim-app.com这样Referer头里就包含了所需的字符串。但是这里有一个非常重要的现代浏览器行为变化出于隐私考虑许多浏览器如Chrome、Safari默认会在Referer头中剥离URL的查询字符串和片段部分。这意味着你精心构造的?victim-app.com可能不会被发送到服务器。2.2.3 强制浏览器发送完整URL含查询参数为了让绕过生效攻击者需要控制恶意页面的响应头强制浏览器发送完整的URL。这通过设置Referrer-Policy响应头实现HTTP/1.1 200 OK Content-Type: text/html Referrer-Policy: unsafe-url !DOCTYPE html ...设置Referrer-Policy: unsafe-url后从该页面发出的所有请求其Referer头都会包含完整的源URL包括协议、主机、路径和查询字符串。这样包含victim-app.com的查询参数就能被成功发送到目标服务器从而可能绕过基于字符串包含的校验。实操心得在测试Referer绕过时务必使用与目标用户群体一致的浏览器版本和默认配置进行测试。Burp Suite等工具捕获的请求可能包含完整URL但这不代表真实浏览器环境也会如此。浏览器的隐私沙盒Privacy Sandbox和默认Referrer策略在不断演进昨天的POC今天可能就失效了。2.3 Referer校验的局限性总结基于以上分析单纯依赖Referer校验是极不安全的主要原因如下可靠性问题Referer头是浏览器可选添加的用户或浏览器扩展可以禁用它。一些网络环境如从HTTPS页面跳转到HTTP出于安全考虑或使用relnoreferrer的链接也会导致Referer缺失。隐私泄露风险完整的Referer可能包含敏感信息如搜索词、会话ID等严格校验它可能与隐私保护策略冲突。易被绕过如上所述存在多种成熟的技术可以操纵或移除Referer。破坏用户体验对于从本地文件file://协议、浏览器扩展页面发起的合法请求Referer可能为空或不符导致合法功能失败。因此Referer校验最多只能作为深度防御Defense in Depth中的辅助手段绝不能作为唯一的CSRF防护措施。更可靠的方法是使用CSRF Token。3. CSRF Token防护构建可靠的同步令牌模式CSRF Token跨站请求伪造令牌是目前业界防御CSRF攻击最有效、最推荐的方法。其核心思想是要求每个状态变更的请求POST、PUT、DELETE等必须携带一个服务器生成的、不可预测的令牌该令牌与当前用户会话绑定。3.1 CSRF Token的工作原理与生命周期一个健壮的CSRF Token机制包含三个关键环节生成、分发、验证。3.1.1 生成确保随机性与唯一性Token必须是高强度的随机数攻击者无法猜测或枚举。通常使用加密安全的随机数生成器CSPRNG。import secrets import hashlib import time def generate_csrf_token(session_id): # 结合会话ID和时间戳增加熵值 raw_token f{session_id}{secrets.token_hex(16)}{int(time.time())} # 使用HMAC或哈希函数生成最终令牌增加服务器验证能力可选 token hashlib.sha256(raw_token.encode()).hexdigest() return token关键点Token不应仅仅是一个随机字符串最好能与会话Session绑定。这样即使Token被泄露例如通过XSS只要会话结束Token也就失效了。3.1.2 分发安全地传递给客户端生成的Token需要放在用户访问的页面中以便在提交表单时能随请求一起发回。最常见的方式是放在表单的隐藏域hidden field里。!-- 服务器端渲染如Jinja2, JSP -- form action/transfer methodPOST input typehidden namecsrf_token value{{ csrf_token }} input typetext nameamount input typesubmit value转账 /form对于单页面应用SPAToken通常通过一个初始的API调用获取然后存储在内存或Web Storage中并在后续的请求头如X-CSRF-Token中携带。// 前端从后端获取Token fetch(/api/csrf-token, { credentials: include }) .then(res res.json()) .then(data { window.csrfToken data.token; }); // 发起敏感请求时在头部携带 fetch(/api/transfer, { method: POST, headers: { Content-Type: application/json, X-CSRF-Token: window.csrfToken }, credentials: include, body: JSON.stringify({ amount: 1000 }) });注意事项绝对不要将CSRF Token通过Cookie发送给客户端进行验证。Cookie是浏览器会自动携带的东西这违背了CSRF防护的初衷攻击者也能利用自动携带的Cookie。Cookie应该只用于维护会话Session ID而CSRF Token应放在请求体或自定义头中。3.1.3 验证服务器端的严格比对当服务器收到请求时必须执行验证从请求中提取Token从表单字段csrf_token或自定义头X-CSRF-Token。从当前用户会话中取出之前存储的Token。使用恒定时间比较constant-time comparison函数比对两个Token是否一致。这是为了防止基于响应时间的时序攻击Timing Attack。import hmac def validate_csrf_token(request_token, session_token): # 使用hmac.compare_digest进行恒定时间比较 if not request_token or not session_token: return False return hmac.compare_digest(request_token, session_token) app.route(/transfer, methods[POST]) def transfer_money(): # 从表单获取Token request_token request.form.get(csrf_token) # 从会话中获取Token session_token session.get(csrf_token) if not validate_csrf_token(request_token, session_token): abort(403, Invalid CSRF token.) # 验证通过处理业务逻辑 # ... # 重要处理成功后可以考虑重新生成Token一次性使用 session[csrf_token] generate_csrf_token(session.sid)3.2 针对CSRF Token的进阶攻击与防护没有绝对的安全。即使使用了CSRF Token如果实现不当依然可能被绕过。3.2.1 令牌验证逻辑缺陷依赖请求方法有些应用只对POST请求验证Token而对GET请求不验证。攻击者可以将有害操作构造在GET请求中例如img src/delete_account?confirmyes从而绕过防护。原则所有状态变更的请求无论方法都应验证Token。令牌存在性校验和Referer一样如果应用逻辑是“有Token就校验没有就不校验”那就形同虚设。原则对于需要防护的端点Token是必须的缺失即拒绝。令牌未绑定会话Token如果全局通用或者绑定的是另一个不安全的Cookie而非会话Cookie攻击者可以先用自己的账户获取一个Token然后诱导受害者使用这个Token。原则Token必须与当前用户的会话ID强绑定。3.2.2 令牌泄露与滥用通过XSS窃取Token如果网站存在XSS漏洞攻击者可以注入脚本读取页面中的Token然后构造一个包含正确Token的伪造请求。这凸显了纵深防御的重要性CSRF Token不能防止XSS而XSS可以摧毁CSRF防护。必须同时做好输入输出编码、内容安全策略CSP等来防XSS。Token放在Cookie中这是一个灾难性的错误。如果服务器从Cookie中读取Token进行验证即“双重Cookie提交”模式的一种错误实现攻击者可以通过子域名Cookie污染如果域名设置不严格等方式进行攻击。重申Token应该放在请求体或头部验证时与会话中的值比对而不是和Cookie中的值比对。3.3 同站CookieSameSite Cookie一道重要的辅助防线除了CSRF Token现代浏览器提供的SameSiteCookie属性是防御CSRF的利器。它可以控制Cookie在跨站请求中是否被发送。SameSiteStrict最严格。Cookie仅在同站请求即当前页面的站点与请求目标站点一致中发送。这意味着从其他网站链接过来时用户即使已登录也不会携带会话Cookie服务器会将其视为未登录状态。这提供了最强的防护但可能影响用户体验例如从邮件链接点回网站需要重新登录。SameSiteLax默认值平衡安全与体验。在跨站的顶级导航如点击链接的GET请求中会发送Cookie但在跨站的POST请求或通过img,script等标签发起的请求中不发送。这能有效阻止大多数CSRF攻击因为CSRF通常通过表单POST或自动发起的GET实现同时保持了主要导航流程的可用性。SameSiteNoneCookie在所有上下文中发送但必须同时设置Secure属性即仅限HTTPS。设置示例在HTTP响应头中Set-Cookie: sessionidabc123; Path/; HttpOnly; Secure; SameSiteLaxSameSite的绕过与注意事项 尽管SameSiteLax是强大的默认防护但并非无懈可击。GET型CSRF如果应用允许通过GET请求进行状态变更如GET /delete?id1SameSiteLax不会阻止此类请求携带Cookie。因此切勿用GET方法实现有副作用的操作。同站Same-Site攻击如果攻击者能控制目标站点的子域名sub.victim.com或者利用站点的其他功能如用户评论、文件上传注入攻击载荷那么发起的请求属于“同站”请求SameSite限制无效。这需要结合其他安全措施如严格的子域名隔离、用户内容沙箱化等。4. 实战部署构建多层CSRF防御体系在实际项目中我们应该采用“不把鸡蛋放在一个篮子里”的策略构建多层次的防御。4.1 防御策略组合拳首选且核心使用同步令牌模式的CSRF Token。为每个用户会话生成唯一、随机的Token。将Token嵌入到所有状态变更请求的表单或请求头中。在服务器端严格验证Token的有效性和匹配性。考虑对高敏感操作如转账、改密使用一次性Token。关键辅助为会话Cookie设置SameSiteStrict或Lax属性。对于绝大多数应用将主要的会话Cookie设置为SameSiteLax是安全且用户体验友好的最佳实践。对于后台管理、金融操作等极高安全要求的子系统可以考虑使用SameSiteStrict。深度防御实施自定义请求头校验。对于通过AJAX/Fetch API发起的请求要求携带一个自定义头如X-Requested-With: XMLHttpRequest。由于浏览器同源策略SOP的限制普通form提交或img标签无法添加自定义头这能有效阻挡简单的CSRF攻击。但注意如果CORS配置不当此方法可能被绕过。谨慎参考实施严格的Origin/Referer校验。作为Token和SameSite的补充可以对敏感请求校验Origin或Referer头。Origin头在跨域请求中更可靠且不会包含路径信息隐私泄露风险更低。必须注意处理头缺失的情况不能因为头缺失就放行。逻辑应该是“如果存在Origin/Referer头则必须校验通过如果不存在则请求必须包含有效的CSRF Token”。这可以防止攻击者通过移除头部来绕过。4.2 不同技术栈下的实现要点Spring Security (Java)默认已启用CSRF防护。它会自动生成Token_csrf你需要确保在所有非只读non-idempotent的表单中包含它使用Thymeleaf的th:action会自动添加或手动添加input typehidden name_csrf th:value${_csrf.token}/。对于REST API你可能需要根据情况调整或禁用CSRF并结合其他如JWTSameSite Cookie的防护。Django (Python)中间件django.middleware.csrf.CsrfViewMiddleware默认启用。在模板中使用{% csrf_token %}标签。对于AJAX请求需要从Cookie中读取csrftoken并设置在X-CSRFToken请求头中。Express.js (Node.js)可以使用csurf中间件注意其维护状态或csrf-csrf等库。务必配合cookie-parser和会话中间件使用。注意在SPA中正确配置Token的获取和发送方式。现代前端框架 (React/Vue/Angular)Token通常通过一个初始请求从后端获取存储在内存或状态管理库中。使用HTTP拦截器Interceptor或请求封装库如axios自动为每个非只读请求添加X-CSRF-Token头。切记不要将Token存储在localStorage或sessionStorage中以防XSS攻击导致泄露。5. 常见问题排查与安全测试实录在开发和测试过程中CSRF防护相关的问题层出不穷。下面是我整理的一些典型场景和排查思路。5.1 开发与集成中的常见坑点问题1Token验证总是失败返回403。检查点1Token是否被正确包含在请求中使用浏览器开发者工具的“网络”Network选项卡查看实际发出的请求负载Payload或头部确认Token字段存在且值非空。检查点2前后端Token存储和获取的键名是否一致前端提交的字段名是csrf_token后端却在csrf-token里找当然找不到。检查表单字段名或请求头名。检查点3会话Session是否一致Token是绑定到会话的。如果前端请求没有正确携带会话Cookie例如fetch请求未设置credentials: include后端就会在一个新的、空的会话里找Token自然找不到。确保API请求的凭证模式正确。检查点4Token是否过期或被刷新检查后端逻辑是否在每次验证后或登录/登出时重新生成了Token如果是前端需要用新的Token更新后续请求。问题2使用SameSiteLax后从第三方网站跳转回来需要重新登录。分析这是SameSiteLax的预期行为。对于通过a链接的跨站顶级导航GET请求会携带Cookie通常不会导致登出。但如果用户是通过OAuth授权回调、支付回调等POST形式的跨站请求返回Lax策略下Cookie不会被发送导致会话丢失。解决方案对于关键的、预期会有跨站POST回调的端点如OAuth callback URL可以考虑为该特定请求使用SameSiteNone; Secure的Cookie或者确保回调流程不依赖主会话Cookie例如使用一次性code换取令牌。5.2 安全测试中的验证与绕过尝试在对应用进行渗透测试时如何验证CSRF防护是否有效基础测试尝试直接构造请求使用Burp Suite的“生成CSRF PoC”功能对一个敏感操作如修改邮箱的请求生成测试HTML。在浏览器中最好是与受害者环境隔离的浏览器实例登录目标应用然后打开生成的HTML文件。观察操作是否成功。如果成功说明防护完全缺失或存在严重缺陷。测试Token防护移除/修改Token在重放请求中删除或随意修改Token值看请求是否被拒绝。复用Token用一个用户的Token替换到另一个用户的请求中看是否被拒绝测试会话绑定。测试Token存在性依赖直接发送一个没有Token字段的原始请求看是否被拒绝。测试Referer/Origin校验移除头部在Burp Repeater中将请求的Referer或Origin头删除重放请求看是否通过。修改头部将头部值改为一个攻击者控制的域名如https://attacker.com或包含目标域名的其他域名如https://target.com.attacker.com看校验逻辑是否足够严格是否检查精确匹配、是否检查协议和端口。测试SameSite Cookie检查应用设置的会话Cookie是否包含SameSite属性。尝试从另一个域发起跨站POST请求使用浏览器工具或编写简单页面测试Cookie是否被发送。一个真实的踩坑记录在一次测试中目标应用对/api/change-password端点实施了CSRF Token校验但对/api/profile/update端点却没有因为后者被开发者误认为是“非敏感”操作。然而update接口允许修改登录邮箱。攻击者通过CSRF修改了受害者的邮箱然后利用“忘记密码”功能重置了密码从而完全接管账户。教训CSRF防护的范围必须覆盖所有会导致状态变更的端点不能凭主观判断“敏感性”。6. 总结与最佳实践清单经过上面这些拆解我们可以把构建健壮CSRF防护的核心要点浓缩成下面这个可操作、可检查的清单强制使用CSRF Token为每个用户会话生成高强度随机Token并将其嵌入所有可能改变状态的请求不仅仅是POST还包括PUT、PATCH、DELETE等中。Token必须存储在服务器端会话里。实施严格的服务器端验证对于受保护的端点必须验证请求中的Token是否与会话中存储的Token一致。验证逻辑必须严谨不能因为Token缺失或格式错误就跳过。使用恒定时间比较函数。正确设置Cookie的SameSite属性将主要的会话Cookie设置为SameSiteLax这是现代浏览器的默认值但显式声明更安全。对于需要跨站POST请求的特定场景如第三方登录回调谨慎评估并使用SameSiteNone; Secure。实施深度防御对敏感请求额外校验Origin或Referer头。逻辑应为“若存在则必须合法若不存在则必须依赖Token”。为API请求要求自定义头如X-Requested-With但这不能作为唯一防护。安全的Token传递与存储不要通过Cookie发送用于验证的Token。在前端Token应放在表单隐藏域或HTTP请求头中。避免将Token持久化在localStorage/sessionStorage中以防XSS。对于SPA可放在内存或仅会话期内有效的存储中。遵循RESTful和安全设计原则不要用GET请求执行有副作用的操作。GET请求应幂等且安全。严格实施用户输入验证和输出编码防止XSS漏洞因为XSS可以绕过CSRF防护。部署内容安全策略CSP进一步限制脚本执行源增加攻击难度。定期进行安全测试与审计将CSRF漏洞扫描纳入CI/CD流程。定期使用Burp Suite、OWASP ZAP等工具进行手动和自动化的测试。对自研的防护逻辑进行代码审查重点关注Token生成、存储、验证的每一个环节。安全是一个持续的过程而不是一个可以一劳永逸开启的开关。CSRF防护看似基础但细节决定成败。理解每一种防护机制的原理和局限进行正确的组合与配置才能为你的Web应用筑起一道坚实的防线。在实际操作中最省心的方法依然是优先使用成熟框架内置的、经过广泛验证的CSRF防护模块并确保按照官方最佳实践进行配置而不是盲目地自己从头造轮子。