上一章处理文件上传、下载和路径问题时,我们一直在追问:一段输入进入文件系统后,会被谁读取,又会被解释成什么。到了业务逻辑安全,观察位置要再往上移一层。请求的格式可能完全正确,参数也没有注入字符,用户甚至通过了登录;可如果系统允许他在错误的时间、对错误的对象、用错误的顺序完成一个动作,最后仍会得到不该出现的业务结果。
这也是逻辑漏洞最让人头疼的地方。扫描器能告诉你“数量是整数”,却不知道“每个账户每月只能领取一次练习券”;它能看到确认接口返回成功,却不知道一笔退款不能超过已结算金额,更不知道提交申请的人不该审批自己的申请。要判断系统是否安全,我们得先理解业务承诺,再验证服务端能不能在每条入口、每次重试和每个并发时刻都守住承诺。
本章只讨论书面授权的隔离靶场、支付沙箱和由业务方准备的测试数据。涉及价格、库存、优惠、积分、额度、退款、审批和外部回调时,必须提前约定账户、对象、单次成本、累计预算、最大并发度、停止条件与回滚负责人。不要触发真实扣款、真实发货、真实权益发放、客户通知或第三方结算;证明一条不变量被破坏后就应停止,不要把验证扩大成套利或压力测试。
逻辑测试最容易走偏的方式,是打开代理工具后盯着请求体找“看起来能改”的字段。价格能不能改、状态能不能改、数量能不能变成负数,当然都值得看,但这仍然只是从实现细节出发。真正稳定的起点是业务不变量:无论请求从网页、移动端、旧版接口、后台任务还是第三方回调进入,某条规则都必须成立。
你可以把不变量理解成系统的“底线”。页面布局会变,接口名字会变,服务也可能拆分,但“已完成退款的总额不能超过实际收款”“一个虚拟名额最多归属一个预约”“申请人不能审批自己的申请”不会因此改变。

一条可测试的规则最好同时包含四部分:前置条件、业务动作、允许的结果和禁止的副作用。比如“处于待审批状态、且审批人与申请人不同的测试申请,才能进入已批准;失败时不能创建发放任务”。这比“审批接口要安全”具体得多,也能直接转成测试与回归用例。
业务说明常写成“优惠仅限新用户”“库存不足时不能下单”“取消后原路退回”。这些句子还不够精确。我们要继续追问:
这些问题没有放之四海而皆准的答案。测试者不能凭经验替业务方发明规则,但可以把模糊处指出来,让产品、开发、财务、风控和运营共同确认。规则尚未确认时,先做只读建模,不要用真实状态变更来“猜答案”。

登录成功只是知道“谁在发请求”,并不等于这个人现在可以做这件事。业务动作通常还依赖对象归属、租户、角色、当前状态、额度、时间、前置证据和风险等级。少看一个维度,都可能把权限判断做成一句过于宽松的“已登录即可”。
我们可以用六个问题快速拆解动作:
特别留意“谁在行动”和“请求里写了谁”的区别。userId、tenantId、role、approvedBy 这类字段即使由前端自动填充,也仍是客户端输入。服务端应从经过验证的身份上下文得到操作者,再判断他是否有权管理请求中指向的对象。后台任务和回调也不能天然获得无限权限,它们需要可验证的服务身份和明确的操作范围。
客户端可以表达“我想买哪个测试商品、数量是多少、选择哪张测试券”。这些是意图。单价、库存、优惠资格、税费、对象归属、账户角色和最终应付额是安全相关事实,应由服务端从可信数据重新取得或计算。
这条边界也适用于系统内部。某个值从另一个微服务传来,不代表它自动可信。服务之间要约定消息来源、字段含义、版本、唯一事件标识和验签方式;消费者仍要检查本地业务状态,不能因为消息名字叫“支付成功”就跳过订单归属与金额核对。
在设计异常用例前,先用全新的测试对象完成一次批准的正常流程。记录每一步的参与者、输入、服务端状态、预期副作用、关联标识和耗时。基线能回答三个很实用的问题:系统正常时到底会写哪些记录,异步动作需要多久出现,清理脚本是否真的能恢复测试数据。
先由业务方确认测试账户、虚拟商品、不可兑换额度、通知隔离和第三方沙箱,写清允许改变的对象与绝对禁止触发的下游系统。
用一个全新对象按正常顺序完成流程,只记录观察结果,不做任何变形。把页面动作、服务端状态、账本变化和异步事件对齐。
把观察到的规则写成不变量,再请业务负责人确认例外,例如人工补单、超时补偿、部分退款和管理员纠错。
为每条不变量准备独立测试对象。后续一次只改变一个条件,避免前一轮残留状态污染结论。
多步骤流程在页面上看起来像“第一步、第二步、第三步”,但浏览器里的进度条只负责引导用户。真正的约束必须在服务端:读取对象当前状态,核对操作者和前置证据,执行允许的转换,并把状态变化与副作用放进受控的一致性边界。

假设隔离靶场有一条培训预约流程:
草稿 → 已报价 → 待确认 → 已确认 → 已完成
在满足条件时,草稿、已报价和待确认可以进入已取消;已确认是否允许取消,则取决于业务方给出的锁定规则。为了让状态机能落地,每条转换都要写清以下内容:
状态名称只是标签,守卫条件才是安全核心。“待确认”并不能自动证明已经完成身份核验、报价仍有效或对象归属正确。服务端应在执行敏感动作的那一刻重新核对这些证据,而不是依赖几分钟前页面得到的结果。
每一种偏差都要检查拒绝后的副作用。有的系统会返回失败,却已经写入库存占用;有的接口返回“已处理”,却再次投递了发放消息。响应只是证据的一层,最终状态和下游结果同样重要。
业务规则常在主站实现得很严,旧版接口、移动端、运营后台、批量任务或回调处理却少了一条检查。测试时不要只按页面目录整理接口,而要按“业务能力”整理入口:哪些入口都能确认订单、发放权益、变更角色、修改库存或触发退款。
防御实现可以抽象成下面这段伪代码:
操作者 = 从服务端身份上下文取得()
对象 = 在当前租户中读取并锁定(对象标识)
确认操作者对对象有当前动作权限()
确认对象状态允许目标转换()
确认前置证据存在、未过期且绑定同一对象()
执行业务写入与必要副作用()
记录旧状态、新状态、规则结果与关联标识()如果一个入口省略了其中任意一步,就要解释为什么。所谓“内部接口”不能成为默认放行的理由,因为配置错误、消息伪造、旧客户端和被盗服务凭证都可能让内部入口暴露出与公开入口相同的业务能力。
并非所有回退或人工纠错都是漏洞。业务可能允许授权客服撤销、补录或修正错误记录。关键是例外路径也要有明确角色、理由、二次确认、范围限制、完整审计和补偿规则。发现意外转换时,先向流程负责人确认预期,再判断是否构成问题。
输入校验经常停在“是不是数字、长度对不对、字符是否合法”。逻辑安全还要问:这个值放在当前账户、当前商品、当前时间和当前流程里是否合理。数量 2 可以是合法整数,但当库存只有 1 时,它就是不合法的业务输入;优惠码格式正确,也可能不属于这个账户或不能用于这类商品。

一个金额通常经历展示、报价、确认、结算和退款。测试时要知道每个阶段使用的是实时规则还是快照,快照由谁生成,何时过期,后续动作如何证明自己绑定的是同一份报价。
服务端计算至少要明确:
浏览器可以展示计算结果,但服务端不能把展示值当成记账依据。隐藏字段、禁用输入框和 JavaScript 变量都可以被修改。安全做法是只接收选择和数量,再由服务端重新查询规则、生成有期限的报价快照,并在确认时核对快照状态、对象归属与关键字段。
在隔离数据中,边界用例应围绕业务定义,而不是追求奇怪数字。对于数量,可以检查最小允许值、恰好等于上限、比上限多一、等于虚拟库存和比库存多一。对于每日额度,可以检查本次操作合法但累计后超限的情况。对于日期,可以检查起止顺序、时区边界、过期点前后和被禁止的日期组合。
组合校验往往更容易被漏掉:
“每人每天三次”至少涉及主体、动作、时间窗、渠道和计数时点。主体按账户还是实名身份?网页和移动端是否共用额度?失败请求是否计数?自然日按哪个时区?并发到达的第四次请求怎样被拒绝?管理员代操作是否占用用户额度?
这些答案应落到服务端的统一计数与约束中。只在页面按钮上显示“剩余次数”,或者让每个入口各自维护计数,都会让相同业务能力出现不一致的上限。
访问控制章节里常说“每个请求都要授权”。业务逻辑场景还要更具体:权限不是一张静态角色表,而是“这个操作者此刻能否对这个对象执行这个动作”。同一个客服可以查看本组织的测试订单,却不能查看另一个租户的数据;审批人可以批准部门申请,却不能批准自己创建的记录;离职人员的旧审批链接不能继续生效。
权限检查要靠服务端事实,不靠页面来源、按钮是否可见或跳转路径。尤其要警惕“先查对象有权限,过一会儿再执行动作”的分离:检查后对象的归属、状态或角色可能已经改变。高风险动作应尽量在同一受控事务中重新读取和验证必要状态。
客服找回账号、人工退款、手工补发和例外审批即使由员工操作,仍然有状态、证据、角色和终态。验证这类流程要由业务方安排专门测试人员和虚构资料,不能临时联系真实客服、隐瞒测试身份,也不能对无关员工做社会工程。
更稳妥的方法是桌面推演:业务方准备测试工单,操作员按正常流程处理,测试人员观察操作台是否显示对象归属、风险提示、历史记录和职责冲突,系统是否要求复核,以及例外决定是否留下理由和审计记录。
这几个词经常被混在一起。先把关系拆开:重复提交是同一个业务意图被发送多次;并发是多个执行在时间上重叠;幂等描述同一操作重试后的外部结果;事务解决一组本地读写要么一起成功、要么一起失败。它们互相关联,却不能彼此替代。

很多并发漏洞都长成“先检查,再修改”:
读取剩余名额 = 1
如果剩余名额 > 0:
创建预约
剩余名额减 1顺序执行时没有问题。两个请求同时读取到 1,就可能都通过检查,然后各自创建预约。问题不在于服务器“太慢”,而在于判断和写入之间存在可被另一条执行插入的时间窗口。
常见的敏感临界区包括:检查券未使用后核销、检查余额后扣减、检查退款累计后创建退款、检查库存后占用、检查用户名不存在后创建账户、检查名额后预约、检查一次性任务未完成后发放权益。
幂等键适合处理客户端超时后的安全重试。一个稳妥的设计通常包括:
幂等键不会自动修复越权、错误价格或非法状态转换。如果同一用户为两次领取分别换两个键,而数据库没有“一人一次”的唯一约束,业务规则仍会被破坏。反过来,如果两个完全不同的请求误用了同一个键,系统也不能无条件返回第一次结果,必须识别请求指纹冲突。
本地数据库里常见的防守方式有四类:
“开了事务”不等于已经安全。要看事务范围是否覆盖检查和写入,隔离级别是否符合数据访问方式,异常是否真的回滚,外部调用是不是在持锁时发生。只在应用层先查一次、然后另开事务写入,竞争窗口仍然存在。
数据库事务通常管不到支付沙箱、消息队列和第三方服务。跨服务流程要明确哪条记录是事实源,事件如何唯一标识,消息重复与乱序怎样处理,失败后是重试还是补偿。
例如确认测试预约时,可以先在本地事务里完成状态转换、占用虚拟名额并写入待发送事件,再由可靠的发布流程把事件交给下游。消费者以事件标识和业务对象做去重,并再次检查自己负责的不变量。取消动作若需要释放名额、撤销测试凭证和恢复不可兑换额度,应记录每个补偿步骤的状态;补偿失败不能悄悄吞掉,而要进入可追踪的待处理队列。
幂等表示“同一业务意图重复到达不会重复生效”,不表示“所有请求都应该成功”;事务表示一组受管数据操作的一致提交,不表示外部副作用也自动回滚。修复并发问题时,要先指出被保护的不变量,再选择唯一约束、条件更新、锁、版本控制、幂等和补偿的组合。
业务用例需要具体,否则容易变成“检查一下逻辑”这种无法执行的建议。下面三组练习都假定使用虚拟库存、不可兑换优惠和支付沙箱,测试对象彼此独立。目标是用最少动作判断规则是否成立,不是追求最大收益或构造可用于真实系统的自动化脚本。
先确认库存在哪一步扣减,以及草稿、待支付、已确认、取消和超时分别怎样处理。准备一个只有少量虚拟名额的测试活动,分别执行正常确认、确认前取消、占用后超时和一次两请求并发。每轮只使用新的测试对象。
要核对的不只是页面剩余数:
如果两个请求就已经让库存变成负数或产生两个有效占用,证据足够。立即停止并执行回滚,不要增加线程数来“证明更严重”。
优惠规则往往散落在领券、购物车预览、结算、支付确认和取消恢复等多个阶段。先由业务方给出一张无现金价值的测试券,再检查:
这里最容易误判的是“券又变成可用”。有的活动明确允许支付失败后恢复,有的则在下单时就消耗资格。测试结论必须对照已确认的规则,不能把产品设计差异直接写成漏洞。
退款验证必须使用支付平台沙箱、零价值通道或完全隔离的账本。不要对真实交易试探少量退款,也不要重放真实回调。准备一笔可控测试收款,先记录原始金额、币种、可退款余额和订单状态。
最小用例可以覆盖:一次正常部分退款、同一退款请求的安全重试、两次部分退款累计恰好到上限、再多一笔越过上限、对未结算订单退款、取消后重复退款,以及退款回调重复或乱序到达。每次只推进到能证明规则的最低影响状态。
安全结果应满足:累计退款不超过可退款金额;金额和币种绑定同一原交易;同一退款意图只创建一次外部动作;订单与退款单状态能够对账;失败与补偿有清楚记录;退款权限、对象归属和必要复核在执行时重新检查。
并发问题需要真实的时间重叠,但“并发越高越好”是错误目标。我们只需要回答:当两个受控操作竞争同一份测试资源时,不变量是否还能成立。最大并发度、总请求数和测试窗口必须写进授权范围。
用对象 A 顺序执行两次相同动作,记录系统对正常重试的基线结果,包括响应、主记录、计数器、账本和消息。
准备条件完全相同的对象 B,确认它只连接虚拟库存、测试额度和隔离通知,并让日志能够按关联标识检索。
按批准方式让两个请求尽量同时到达。只发这一组,不循环,不增加批量账户,也不对生产第三方服务执行。
等待约定的异步收敛时间,然后核对最终不变量。若出现两个成功记录、重复副作用、负数计数或互斥终态,立即停止。
两个请求都返回“已受理”不一定有漏洞,系统可能在异步消费者中去重;一个成功一个失败也不一定安全,失败请求可能已经扣过库存。并发验证至少检查四层:
结论应写成不变量的实际结果,例如“虚拟库存为 1 时,两次并发确认最终留下两个有效预约”,而不是“接口存在竞态”。前一种描述能让开发人员直接定位该保护什么,后一种只给出了技术标签。
有些滥用不依赖跳步或错误状态。每次预约、发帖、领取或购买单独看都合法,但如果自动化规模超过业务承受范围,就会占光名额、制造垃圾内容、扭曲活动经济或让真实用户无法使用。这类问题更像“业务能力缺少使用预算”,不能只靠登录限速解决。
敏感业务流常包括:限量购买、预约、邀请码、注册奖励、积分发放、试用开通、内容发布、导出任务、验证码发送、退款申请和账户恢复。风险与行业和产品规则有关,同样的高频发帖,在社区可能是垃圾信息,在开放数据平台也可能是正常自动化。
控制要贴近具体功能,可以组合:
风控是第二道防线,不能替代正确性约束。验证码可能增加批量操作成本,却不能修复一张券可以重复核销;限速能减缓并发,却不能保证两个请求不会同时越过库存检查。
业务方应提供专用策略、测试标签或隔离租户。测试目标是确认限额在服务端执行、不同入口口径一致、超过阈值后安全降级、人工复核能看到足够上下文。不要批量注册真实账户,不要反复发送短信或邮件,也不要为了触发告警而制造大规模行为。
逻辑滥用常使用格式正确的请求,Web 防火墙未必会报警。蓝队需要知道谁对哪个对象做了什么、动作前后状态怎样变化、哪条规则拒绝了请求,以及最终有没有产生价值或外部副作用。
建议为确认、核销、退款、角色变更、审批、账户恢复和库存调整等动作记录:认证主体、租户、目标对象、业务动作、旧状态、新状态、服务端计算的关键值、规则结果、拒绝原因、幂等键或事件标识、请求关联标识、入口类型与必要的网络设备上下文。
日志里的敏感信息要最小化。不要为了审计记录完整卡号、验证码、令牌或隐私正文。业务审计应与普通调试日志分开保存,限制修改与删除权限,并让关联标识能把网关、应用、队列和下游服务的记录串起来。

阈值要从正常基线出发。支付沙箱短暂超时会让幂等重试一起上升,新版本发布可能让非法转换突然增加。告警不能只看单个数字,要结合最终记账、客户端版本和基础设施事件,避免把系统故障误判成攻击。
发现异常后先控制影响并保存证据,再修正业务状态、处理补偿和核对账本。随后要问:哪条服务端规则本应阻止它?如果每次都靠人工封禁账户,同一个缺口还会再次出现。处置结果应回到唯一约束、状态守卫、额度控制或上下文授权,并增加相应回归用例。
逻辑测试的质量不由请求数量决定。好的验证会明确假设,只改变一个条件,用最少动作观察一个不变量,并能在完成后恢复环境。下面这张工作表可以直接用于测试设计。
先观察正常流程与所有入口,再提出一个可证伪的假设,例如“服务端可能只在页面上限制一次性领取”。验证时使用一个无价值测试资格,顺序重试一次;只有在授权允许且顺序基线无法说明问题时,才使用一组两请求并发。完成后核对唯一记录和副作用,恢复测试对象并保存关联标识。
如果已经看到非法终态,不必继续走到真实发货、真实提现或真实通知。服务端状态、隔离账本和审计事件通常足以证明问题。验证的目标是让业务方相信规则确实被破坏,不是展示破坏能扩大到什么规模。
一份高质量证据链应该能回答:动作前对象是什么状态,操作者为何本不应成功,系统实际接受了什么,最终哪条不变量失效,以及测试数据是否已经恢复。只截一张“成功”页面,通常无法回答这些问题。
“存在业务逻辑漏洞”几乎没有修复价值。标题应直接写结果,例如“已取消的测试预约仍可进入完成状态”或“同一测试退款请求并发执行后创建两条有效退款记录”。正文先说明正常规则,再给出最小偏差和最终业务状态。
第一层是可信事实:价格、角色、对象归属、额度和状态来自服务端可信来源。第二层是状态与授权:每次敏感动作都核对当前状态、操作者、对象、前置证据和有效期。第三层是原子性:数据库约束、条件更新、锁或版本控制直接守住不变量。第四层是跨服务一致性:事件唯一、消费者幂等、失败可补偿。第五层是可观测性:拒绝、冲突、重试和补偿都能关联到同一业务对象。
前端隐藏按钮不能修复跳步,验证码不能修复错误价格,接口限速也不能保证一次性权益在并发下只生效一次。每项控制都应明确对应哪条规则,否则很容易在“加了防护”的表面下保留原问题。
修复后先确认正常用户还能完成流程,再重跑原始最小反例。随后检查失败请求没有留下副作用,旧版入口和异步消费者执行同一规则,过期中间状态能够清理,两个请求竞争时最终状态仍一致,日志能解释拒绝和冲突原因。
涉及库存、优惠、积分和退款时,回归结束要由业务方做一次测试账本对账。只有业务对象、计数、流水、消息和外部沙箱记录都收敛,问题才算真正关闭。
假设靶场提供“安全训练营预约”:每个测试账户有 1000 点不可兑换额度,每场活动有 2 个虚拟名额,每个账户同一活动只能确认一次。流程包括草稿、报价、待确认、已确认、已完成和已取消,所有通知只进入测试收件箱。
先写五条不变量:报价由服务端按测试规则计算;报价过期后不能确认;一个虚拟名额最多归属一个有效预约;同一账户同一活动只能确认一次;已完成预约不能取消并恢复额度。再为每条规则准备独立对象,完成正常基线、跳步、过期、边界数量、重复确认与两请求竞争。
测试过程中不修改真实金额,不连接生产支付,不扩大并发。发现非法终态、重复占位、额度不守恒或重复通知后立即停止。最后核对预约状态、虚拟名额、测试账本、消息事件和审计日志,再由业务方重置所有对象。
业务逻辑测试可以压缩成一条清楚的路线:先取得授权并隔离真实价值,再把流程写成参与者、对象、状态和不变量;用正常基线理解系统,用最小反例检查顺序、归属、金额、额度与次数;只在单独批准后做低并发验证;最后核对响应、持久状态、账本和副作用,并把修复落到服务端守卫、原子约束、幂等、补偿和监控上。
这一章反复强调“客户端表达意图,服务端确认事实”。下一章会把视线移到客户端攻击。我们会继续检查 JavaScript、浏览器存储、跨窗口消息和浏览器信任边界,理解哪些数据可以留在客户端帮助交互,哪些权限、状态和关键计算绝不能只靠客户端决定。
由业务方执行回滚并对测试账本。现象不稳定时,优先和开发人员查看事务边界、锁等待与事件日志,不向系统反复加压。