去年冬天,逻辑漏洞我在一个技术论坛上看到有人提问:为什么他写的缺陷登录验证代码,用 `if (user.isAdmin == true)` 判断管理员权限,逻辑漏洞结果普通用户只要把浏览器里的缺陷某个参数改成 `true` 就能进后台?底下跟帖的人笑成一片,但笑完之后,逻辑漏洞没人真正解释清楚问题出在哪。缺陷其实这就是逻辑漏洞最典型的逻辑缺陷漏洞——代码逻辑本身没按预期运行,跟缓冲区溢出、缺陷SQL注入那些“技术性”漏洞完全不是逻辑漏洞一回事。
逻辑漏洞的缺陷核心特征就一条:程序能跑,但跑偏了。逻辑漏洞攻击者不需要懂汇编,缺陷不需要构造恶意payload,逻辑漏洞只需要理解业务流程,缺陷然后找到那条“合法但错误”的逻辑漏洞路径。比如一个电商网站,优惠券只能使用一次。开发者怎么实现的?用户下单时,系统检查订单表里有没有用过这张券。听起来没问题。但攻击者同时开两个浏览器窗口,用同一张券下两单。两个请求几乎同时到达服务器,第一个请求查数据库时第二个还没写入,两边都通过了检查。券用了两次。这不是代码写错了,是代码之间的时间差被利用了。
我见过更离谱的。某平台找回密码功能,第一步验证手机号,第二步验证验证码,第三步重置密码。开发者把用户ID存在了一个隐藏表单字段里。攻击者在第三步抓包,把用户ID改成别人的,密码就重置到别人账号上了。整个流程每一步都“逻辑正确”,但拼在一起就成了提权工具。这种漏洞用扫描器扫不出来,因为扫描器只会看参数有没有特殊字符,不会理解“第三步的user_id应该跟第一步的session绑定”这种业务规则。
为什么逻辑漏洞这么难防?因为它没有固定模式。SQL注入有预编译语句可以一劳永逸,XSS有输出编码可以统一处理。逻辑漏洞呢?每个业务场景都不一样。一个签到功能,每天只能签一次。开发者用“当天是否已签到”来判断。攻击者把系统时间改到明天,签一次,再改回来,再签一次。或者用API直接调用签到接口,绕过前端按钮的disabled状态。你没法用防火墙规则拦住这些,因为请求看起来完全正常。
更麻烦的是,很多逻辑漏洞藏在“正常功能”里。比如修改密码时不需要输入旧密码,只要session有效就行。这算漏洞吗?如果session被劫持了,攻击者就能直接改密码。但开发者觉得“用户已经登录了,当然可以改密码”。这种认知偏差才是逻辑漏洞的温床。我见过一个后台管理系统,删除用户的功能只在前端隐藏了按钮,后端接口没做权限校验。普通用户抓包直接调删除接口,整个用户表没了。开发者后来辩解:“我以为前端隐藏了就够了。”——这不是技术问题,是思维懒惰。
要挖这类漏洞,得换一种思路。别盯着代码里的函数调用,去看业务流程的每一步:这个参数从哪来?能不能改?改了之后系统会信任它吗?两个操作之间的状态检查是不是原子的?有没有可能让系统在“检查通过”和“执行操作”之间插入另一个请求?把这些问号拉直,逻辑漏洞就浮出来了。防御也一样,别指望某个框架能自动解决,得把每个业务规则当成安全边界来设计。比如优惠券只能用一次,那就用数据库唯一索引或者分布式锁,别靠“先查后写”这种有竞态条件的逻辑。
说到底,逻辑漏洞是人和系统之间的理解错位。开发者以为流程是A→B→C,攻击者发现可以A→C→B,或者A→B→B→C。代码没报错,日志也正常,但钱没了,数据泄露了。这种漏洞不会出现在CVE列表里,但造成的损失一点不小。下次写代码时,多问一句:如果用户不按我设计的顺序操作,会发生什么?答案往往就是漏洞本身。