CSRF的2026年真相:你防的是“跨站请求”,攻击者利用的是“你允许状态变更的那个方法”"

admin 2026-11-12 12:18:58 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

CSRF的2026年真相:你防的是“跨站请求”,攻击者利用的是“你允许状态变更的那个方法”

原创

www.klsec.com
www.klsec.com

昆仑AI安全实验室

2026年11月11日 00:00
广东

去阅读

2026年9月,Prospero Flow CRM披露了一个CSRF漏洞,CVE-2026-82911。漏洞的根因被VulDB的分析写得极其精确:Laravel的VerifyCsrfToken中间件只在POST、PUT、PATCH、DELETE上强制执行CSRF令牌检查。OrderConfirmController的订单确认端点,被声明为Route::get('/order/confirm/{order_number}')。GET不在中间件的保护范围内。一个未认证的攻击者,只需要让一个已登录的用户点击一个链接,订单就从“待处理”变成了“已确认”。因为订单号是顺序整数,攻击者可以枚举并一次性确认所有存在的订单。

同一个模式在两个月前出现在wger的CVE-2026-45161里。trainer_login视图接受GET请求并执行django_login(),Django的CsrfViewMiddleware只对不安全方法(POST/PUT/PATCH/DELETE)强制令牌。一个<img src=\"https://target/en/user/2/trainer-login\">标签,就能在教练的浏览器里强制把会话重新绑定到一个任意用户账户上。

这就是2026年CSRF漏洞的核心结构:防御者把“状态变更”和“POST方法”画了等号。攻击者利用的是“状态变更端点被声明为GET”这个事实。 CSRF令牌、SameSite Cookie、Origin校验——这些机制全部默认“危险请求是POST”。当危险请求是GET时,它们集体失效。

一、GET-based CSRF:SameSite=Lax保护的是“跨站POST”,不是“状态变更”

SameSite=Lax的语义经常被误读。它的准确规则是:顶层导航的GET请求,携带Cookie;跨站的POST、PUT、DELETE等非安全方法,不携带Cookie。

这意味着,一个点击领取优惠券的链接,在用户点击时,浏览器执行的是顶层GET导航。SameSite=Lax的Cookie会被附带。服务器收到的请求,携带着用户的完整会话。如果这个GET端点的语义是“确认订单”,那么“点击一个链接”就等于“确认了一个订单”。

HITCON ZeroDay在2026年4月披露的明志科技大学校园门户密码修改漏洞,把这条逻辑推到了密码篡改的层面。后端的密码修改逻辑没有限制HTTP方法,攻击者把一个恶意GET请求伪装成普通超链接,受害者点击触发顶层导航,SameSite=Lax的Session Cookie被自动附带,密码被修改。

Prospero Flow CRM的漏洞之所以能“枚举并确认所有订单”,是因为GET方法还附带了一个额外的便利:它可以被<img>、<script>、<link>等资源标签加载,也可以在用户无感知的情况下被触发(比如一个0x0的图片)。POST需要表单提交,表单提交在跨站场景下需要用户交互(点击按钮),而且SameSite=Lax会阻断跨站POST的Cookie。GET把“用户交互”从“点击一个按钮”降级为“打开一个页面”,同时绕过了SameSite对POST的阻断。

二、Token的2026年失效:你生成的“随机值”,可能在一行XOR运算里就被还原了

即使一个应用正确地使用了CSRF Token,并且只对POST做状态变更,Token本身的实现缺陷仍然可以摧毁整个防御。

AdminNeo的CVE-2026-96629是这个模式在2026年最精确的标本。它的CSRF Token声称使用了“抗BREACH的XOR掩码方案”,但token的格式是(rand XOR secret):rand——两个操作数都被传输了。攻击者只需要观察一个合法的Token,做一次XOR运算,就能恢复出session_secret。恢复之后,攻击者可以离线生成任意数量、任意时刻有效的伪造Token。

更致命的是Token的熵。rand(1, 1e6)意味着session secret只有大约20比特的熵。即使攻击者没有观察到任何Token,一百万次盲猜请求也足以暴力破解出secret。验证函数还使用了==而不是===,打开了PHP类型戏法的窗口。

CVE-2026-96629的PoC给出了完整的利用链:观察一个Token,XOR恢复secret,生成伪造Token,用伪造Token执行DROP TABLE。整个过程不需要知道用户的密码,不需要接触用户的浏览器,只需要一个泄露的Token和一次简单的数学运算。

Token防御的强度,不取决于“有没有生成Token”。它取决于Token的生成是否使用了密码学安全的随机源、Token的验证是否使用了严格比较、以及Token在传输和存储中是否被当作一个秘密来对待。一个rand(1, 1e6)加==比较的“CSRF Token”,在攻击者眼里不是一个屏障,是一个可以被还原的谜语。

三、SameSite的边界:Lax不是“默认安全”,Strict也不是“不可绕过”

SameSite=Lax的默认值让很多应用“看起来”安全了。但Lax的规则里有一条经常被忽略的细节:Chrome的“Lax+POST”窗口。如果一个Cookie在两分钟内被设置,且没有显式声明SameSite属性,Chrome会允许它在跨站POST中被发送。这意味着,一个攻击者如果能够在两分钟窗口内诱导用户设置一个新Cookie(通过一个子域名或一个可控的端点),跨站POST的Cookie阻断就可能被绕过。

Strict的边界更硬,但不是不可逾越的。2026年8月,Chromium的issue tracker披露了一个PoC:通过chrome-untrusted://lens的渲染进程注入,攻击者可以实现对所有HTTPS跳转的通用SameSite=Strict绕过。PoC的结果矩阵显示,strict_session和lax_session在chrome-untrusted://lens的请求中被同时附带,包括直接请求和经过重定向的请求。

这个漏洞的前提是“攻击者能够注入chrome-untrusted://lens的渲染进程”。这本身是一个高门槛的前提。但它的存在说明一件事:SameSite是一个浏览器层面的策略。浏览器层面的策略,有一个浏览器层面的攻击面。 当你的威胁模型里包含“浏览器本身可能被部分攻陷”时,SameSite=Strict不再是一个绝对的保证。

四、登录CSRF:不改变你的密码,改变“你是谁”

大部分CSRF的讨论聚焦于“以受害者的身份执行操作”。登录CSRF攻击的是另一个维度:以攻击者的身份,在受害者的浏览器里建立会话。

Decepticon的CSRF playbook把这条单列出来:“Login CSRF——登录端点没有token,强制受害者进入攻击者的账户,捕获受害者的后续活动。”

攻击的逻辑是:攻击者在自己的账户里预先放入一些东西(搜索历史、购物车、文档、或者一个等待受害者“触发”的恶意载荷)。然后通过一个登录CSRF,把受害者的浏览器会话绑定到攻击者的账户上。受害者接下来在“自己的”账户里输入的任何东西——搜索词、个人信息、支付凭证——都进入了攻击者的账户。攻击者之后登录自己的账户,读取受害者留下的数据。

wger的trainer_login GET端点是一个更极端的变体:它直接把当前已登录用户的会话,重新绑定到另一个用户账户上。一个教练在浏览恶意页面时,他的会话被静默地切换到了一个攻击者选择的用户身份上。他接下来的所有操作,都以那个被指定的用户身份执行。

五、链式利用:CSRF作为“接驳器”,而不是终点

单独一个“修改邮箱”的CSRF,通常被定为中危。但CSRF在2026年的真实定价,取决于它能接驳到哪条链上。

Decepticon的链式目录写得很直接:“CSRF → 账户接管:修改邮箱 → 密码重置 → 完全接管。”“CSRF → 权限授予:伪造role=admin的批量赋值POST。”“子域名XSS → 双提交绕过:从兄弟子域名写入CSRF Cookie,然后伪造。”

“双提交Cookie”模式有一个经常被忽视的前提:CSRF Cookie必须使用__Host-前缀。没有这个前缀,一个兄弟子域名(或者任何一个能被攻击者控制的子域名)就可以写入一个同名的CSRF Cookie,然后在请求体里放入一个匹配的值,双提交校验被绕过。Decepticon的playbook明确把这一点列为“双提交Cookie缺陷”的检测条件:“Cookie没有__Host-前缀 / 可被任何子域名设置” 。

Referer校验的绕过同样直接。<meta name=\"referrer\" content=\"no-referrer\"> 可以抑制攻击者页面向目标站点发送的Referer头。如果服务端的CSRF防护只依赖“Referer必须存在且来自本站”,攻击者页面上的这个标签会让Referer变成空值。很多实现把“空Referer”当作“直接访问”而放行。

六、测试CSRF时,你在测什么:不是“有没有token”,是“状态变更方法是否被令牌机制覆盖”

2026年的CSRF测试,需要从“找表单里有没有hidden token”升级为“追踪每一个改变状态的端点,它的HTTP方法和它的令牌校验是否匹配”。

第一,枚举所有状态变更端点,不管它是GET还是POST。 用Burp的Target → Site map,或者从JS文件里提取路由。重点关注/confirm、/approve、/delete、/update、/reset、/login、/logout这些语义的端点。 对每一个端点,检查它接受的HTTP方法。如果它接受GET,立即测试它是否可以被一个<img>或``触发。Prospero的订单确认、wger的trainer\_login、明志科大的密码修改——全部是GET端点。

第二,测试Token的实现质量,而不只是“有没有Token”。 如果你能拿到一个合法的Token,检查它的格式。如果它是可解码的(比如masked:rand),尝试恢复它背后的secret。如果恢复成功,检查你是否能伪造一个从未被服务端签发过的Token,并且它仍然被接受。AdminNeo的漏洞说明,Token的“存在”和“安全”是两件事。

第三,测试SameSite的实际行为,而不是它的声明。 在Chrome中,检查目标Cookie的SameSite属性。如果是Lax,测试顶层GET导航是否携带Cookie(用`或location=)。如果是Strict,测试**两分钟窗口内的跨站POST**(“Lax+POST”)。如果是None`,CSRF防御完全依赖于Token和Origin校验,没有浏览器层面的兜底。

第四,测试Referer/Origin校验的绕过。 在攻击者页面上加<meta name=\"referrer\" content=\"no-referrer\">,看目标是否接受空Referer的请求。测试子域名是否可以设置一个同名的CSRF Cookie(检查有没有__Host-前缀)。测试Origin: null是否被当作合法来源。

写在最后

2026年的CSRF,不是在“跨站请求”这个层面被攻破的。它们出现在“状态变更端点被声明为GET” 的那个Route::get里,在rand(1, 1e6)生成的Token secret里,在==而不是===的比较里,在两分钟“Lax+POST”窗口里,在没有__Host-前缀的双提交Cookie里,在<meta name=\"referrer\" content=\"no-referrer\"> 那一行HTML里。

CSRF防御的安全假设,是“危险请求是POST,POST会被Token和SameSite拦住”。攻击者做的事情,是让危险请求不是POST,或者让Token和SameSite的拦截在实现层面失效。 你防的是“跨站请求”。攻击者利用的是“你允许状态变更的那个方法”。

下次测CSRF,不要只问“有没有Token”。问:这个改变状态的操作,它是通过哪个HTTP方法触发的?这个方法,有没有被你的令牌机制、SameSite策略、Origin校验,作为一个“需要保护的方法”来对待? 如果答案是否定的,那条缝就在那里。

严正声明

本文所述所有CSRF漏洞分析、CVE案例及测试技术,仅用于已获得明确书面授权的安全测试、代码审计、红队演练和安全研究场景。CVE-2026-82911(Prospero Flow CRM)已在5.15.11中修复;CVE-2026-45161(wger)的修复方案是增加@require_POST并将next参数移至POST body;CVE-2026-96629(AdminNeo)的Token实现缺陷已在对应版本中修复。所有测试必须在SRC平台或客户授权的资产范围内进行,靶场环境应部署在隔离网络中。未授权扫描、测试、攻击行为违反《中华人民共和国网络安全法》第27条及《刑法》第285条,将承担法律责任。技术无罪,边界是法律。

评论:0   参与:  0