关于 business-logic-testing
business-logic-testing 是用于授权测试应用业务不变量、状态转换、数值处理、权限和并发问题的技能。它强调证明持久状态确实发生违规变化,并要求优先使用测试环境、受控账户和预先约定的请求上限。
主要功能
- 01梳理业务不变量
- 02测试角色、操作与资源权限
- 03探测状态转换滥用
- 04检查数值与价值边界问题
- 05在受控请求上限内测试竞态
适用场景
- 01检查优惠券或订单能否被重复使用
- 02测试用户能否操作其他租户的记录
- 03验证并发请求无法造成余额超支
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ briiirussell/ cybersecurity-skills --skill business-logic-testing安装后在 Claude Code 中输入/business-logic-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/concurrency.mdfindings-format.mdfindings.schema.jsonseverity-calibration.mdto-sarif.mjsvalidate-findings.mjsverification.md详细介绍
业务逻辑测试——不变量、状态机滥用、竞态条件
扫描器无法发现的漏洞类别。扫描器匹配语法;而逻辑缺陷是应用「本应保证的事情」与其代码在对抗性或并发使用情况下实际强制执行的内容不一致。本技能是 owasp-audit 中源代码侧竞态模式(TOCTOU、原子队列声明、先预留后操作)的动态对应部分,也是 web-pentest 第 8 阶段深入测试的基础。
从不变量入手:先明确保证,再尝试破坏它。发现问题不等于「我发送了一个奇怪的请求」,而是「我违反了一项既定不变量,并证明持久状态发生了变化」。
授权检查
并发和逻辑测试会修改真实状态。发送任何请求前:
- 获得书面授权,明确列出此特定应用,并确认业务逻辑和并发测试属于授权范围。
- 优先使用预发布环境或测试模式。 使用支付测试模式(例如 Stripe 测试密钥)、预置测试租户,以及你拥有的一次性账户。除非书面授权范围明确列出,否则绝不可测试生产环境中的资金流、库存账本或第三方计费 API。
- 事先商定请求数量上限。并发验证需要数十个并行请求,而不是数千个——明确上限并严格遵守。
- 对每个要测试的角色和租户,都使用你能控制的账户(所有者、非所有者、管理员、两个租户)。
如果缺少其中任何一项,请停止并询问。参见下文的边界。
发送实时请求明确不属于本技能预先批准的工具范围(仅限 Read, Grep, Glob)——并行请求由人工执行,或由人工逐次批准每次工具调用,从而确保吞吐量测试绝不会在无人值守的情况下触发。
第 1 步——梳理不变量
在发起请求之前,先写下系统承诺了什么。以下四类涵盖了大多数资金和工作流漏洞:
- 价值守恒。 金钱、额度、积分、库存和配额不会被创建或销毁,除非经过授权流程。退款 10 美元必须恰好扣除 10 美元额度;兑换优惠券必须减少其剩余使用次数。
- 唯一性。 每条规则下,优惠券只能兑换一次;用户名、邀请或幂等键只能由一个主体占用;订单只能履行一次。
- 单调性。 某些计数器只能朝一个方向变化——已使用的代金券不会恢复为未使用,已发货的订单不会退回待处理状态,已消耗的一次性令牌不会再次通过验证。
- 排他性。 座位、锁、租约或「待审批」记录在同一时间只能由一个参与者占有;双方不能同时拥有同一个互斥资源。
对于每项不变量,记录它在哪里得到强制执行(数据库唯一约束?应用中的 if?还是仅在 UI 中?),以及什么证据能证明违反了它(账本合计不再为零,或本应只有一条记录却出现了两条)。只在 UI 或单个处理程序中强制执行的不变量,就是优先调查对象。
第 2 步——参与者 × 操作 × 资源矩阵
列出整个矩阵,找出应用遗漏的组合:
- 参与者: 匿名用户、已认证用户、资源所有者、非所有者、管理员、两个独立租户、处于工作流中途的用户。
- 操作: 创建、读取、更新、删除、审批、取消、退款、兑换、转移、推进状态。
- 资源: 每个具有生命周期或余额的对象——订单、发票、优惠券、订阅、文档、审批请求、钱包。
为每个单元格填写预期结果(允许/拒绝),然后测试应拒绝的单元格,以及受状态影响的允许单元格。值得关注的失败包括:非所有者执行所有者操作(与 IDOR/BOLA 重叠——参见 web-pentest 第 4 阶段)、参与者执行仅在其他状态下才有效的操作,或同一参与者将只能执行一次的操作执行两次。
步骤 3 — 滥用状态机
画出预期的状态图(购物车 -> 结账 -> 已付款 -> 已发货;草稿 -> 已提交 -> 已批准)。然后攻击这些边:
- 跳过一步。 不完成步骤 2,直接调用步骤 4 的端点。能否在未扣款的情况下进入
paid,或在没有审核人的情况下进入approved? - 重排步骤。 总额锁定后再应用折扣;付款授权后、扣款前更改收货地址。
- 重放一步。 再次 POST 终态转换。“确认”会执行两次吗?一次性令牌会再次验证通过吗?
- 拆分限额。 限额按请求检查,但不汇总检查:将一次超限操作拆成多次未超限操作(分几笔较小的转账取出每日限额;通过多次上传超出单次上传上限,且这些上传没有共享的累计总量)。
- 批准自己的请求。 篡改
approver_id/reviewer_id,让请求者和批准者成为同一个主体。
这些都可能成为步骤 6 中的并发放大器——顺序执行时难以触发的窗口,通过并行请求就能轻松触发。
步骤 4 — 数值和值问题
- 浮点数与十进制数。 以浮点数存储或计算的价格或余额会累积舍入误差。测试
0.1 + 0.2之类的金额、长小数数量,以及接近舍入边界的值;留意是否有一分钱凭空出现或消失。 - 舍入方向。 每个行项目都向客户有利的方向舍入,再把结果累加,会造成价值流失。许多小额项目的总价可能低于按正确方式计算的总价。
- 负数和零金额。 能否订购数量为
-1的商品、转账负数金额(给自己入账),或应用大于小计的折扣,使总额变成负数? - 跨币种退款套利。 用一种按某汇率换算的货币付款,再用另一种货币退款(或按之后的汇率退款),从中赚取差额。测试扣款和退款是否使用不同的汇率快照或不同的币种。
- 类型 / 精度强制转换。 科学计数法(
1e3)、额外精度、前导零,或在金额类型边界处发生的整数溢出。
步骤 5 — 授权过期与 Saga 缺口
- 过期的功能门控和角色。 降级、取消或更改角色后,旧权限是否会一直有效到缓存过期?在撤销权限后立即测试受限操作,并使用更改前签发的令牌进行测试。
- 敏感操作缺少新鲜意图。 提款、打款、更改邮箱或密码、更改 MFA 和创建 API 密钥,应要求进行额外验证(重新认证、通行密钥,或在页面之外确认);登录页面中运行的脚本无法完成这些验证。仅使用现有会话测试每一项:通过浏览器控制台或代理重放请求,不提供新凭证。如果操作成功,那么页面上的任何第三方脚本也能执行该操作(
breach-patternsPattern 11)。该发现的严重程度取决于操作能够转移什么。 - 事件驱动 Saga 缺口。 由事件协调的多服务工作流(订单 -> 付款 -> 库存 -> 履约)没有单一事务。检查以下情况:某一步在前一步的补偿操作执行前就已提交;发生部分失败时,补偿操作(退款、释放库存)从未触发;消费者不具备幂等性,导致重新投递的事件重复生效。动态迹象是:流程中断或重复后,各服务之间的状态出现内部不一致。
步骤 6 — 并发方法
大多数逻辑不变量都通过先检查再执行的顺序来保障;这种顺序执行时安全,并行执行时却不安全。目标是让 N 个请求进入同一个先检查再执行的时间窗口,使检查在任何执行操作提交前通过 N 次。
可排查的模式(源代码侧的对应内容见 owasp-audit A06/A02/A08):
- 幂等键的作用范围是路径,而不是主体。 如果键基于
(endpoint, key),而非(principal, endpoint, key),其他用户的键——或你自己的重放请求——就会复用已完成的结果。使用同一个键提交两个不同的操作;在两个账户间使用同一个键。 - 先写缓存,再提交。 应用先向缓存写入“已预留/待处理”标记,然后再提交到数据库。两个请求都可能在任一请求提交完成前,从陈旧缓存中读到“可用”。
- 丢失更新(读取-修改-写入)。
balance = read(); write(balance - amount)未使用原子递减或版本检查——并发写入者都基于同一次读取进行操作,最后一次写入覆盖其他写入的效果。 - Saga 补偿时机。 在正向步骤与其补偿操作之间的时间窗口内触发并行请求突发。
- 缺少
NX/EX或 fencing token 的 Redis 锁。 未通过原子操作获取锁并设置过期时间,或未使用 fencing token 的锁,会让两个持有者都以为自己拥有锁(一个获取了锁,另一个覆盖设置;或一个因 GC 暂停的持有者在租约过期后仍执行操作)。 - READ COMMITTED 异常。 默认隔离级别允许不可重复读和写偏斜——两个事务分别读取到某项约束均已满足,然后都进行写入,导致违反约束。通过
SELECT ... then INSERT(而非数据库 UNIQUE 约束)强制唯一性时,也会出现此问题。 - Upsert 竞争。 在应用代码中实现“若不存在则插入,否则更新”(而非使用原子
INSERT ... ON CONFLICT),会在并发时重复插入。
让请求落在同一时间窗口内。 相当于 Web 请求中的发令枪:
- HTTP/2 单数据包攻击。 将多个请求的最后一个帧放入同一个 TCP 数据包,使服务器同时收到这些请求,从而消除竞态中的网络抖动。
- 最后字节同步(HTTP/1.1)。 通过保持活动状态的连接提前发送每个请求除最后一个字节外的所有内容,然后同时发送最后一个字节。
- 连接预热。 先发送一个无关紧要的请求,以便在定时突发前完成 TLS/连接设置和任何延迟初始化。
工具、一个可运行的 Turbo Intruder 配置,以及一个精简的 httpx/curl --parallel 运行器,见 references/concurrency.md——所有内容均仅限于授权测试用途,并遵守约定的请求上限。
按类别进行验证与排除误报
在将任何候选问题认定为发现之前,都必须达到证据标准。使用以下判定术语:候选问题为 confirmed、needs_validation 或 rejected(参见 references/verification.md)。严重性只适用于 confirmed 发现;请参照 references/severity-calibration.md 校准。
通用验证标准(适用于所有类别): 1 个请求被正确拒绝后,N 个并发请求成功,而按预期本应只有 1 个成功——且有证据证明持久化状态发生了变化(账本的总和不再正确、要求唯一时出现了两行、计数器超过其单调上限,或余额变动超出任何单个授权操作允许的范围)。单凭响应代码发生变化并不能证明问题;请读取持久化状态进行核实。
| 类别 | 确认条件 | 可能的误报 |
|---|---|---|
| 双重花费 / 优惠券重复使用 | 优惠券使用次数为 N,却被兑换超过 N 次;退款金额超过已扣款金额;库存超卖——通过读取余额/数量予以证实 | 操作确实具有幂等性——由 ETag/If-Match 强制执行,或由 DB UNIQUE/ON CONFLICT 合并重复操作,确保只有一次提交 |
| 更新丢失 | 并发增量的总和与持久化余额的净变化不一致 | 原子递减或版本/WHERE balance >= amount 检查拒绝了除一个以外的所有请求 |
| 幂等键重用 | 第二个不同操作(或另一个主体)重用了某个键并获得了第一个操作的结果,或重放请求导致副作用被重复应用 | 键已按主体 + 请求正文哈希正确划分作用域;重放请求返回缓存结果,不会再次应用副作用 |
| 状态机跳步/乱序/重放 | 在不满足前置条件的情况下,持久化状态进入了终止/禁止状态 | 服务端拒绝了状态转换;只有 UI 前进了 |
| 拆分请求绕过限制 | 跨拆分请求的总量超过限制,且该超额已持久化 | 服务端运行中的总量检查捕获了累计值 |
| 数值问题(负数/舍入/套利) | 最终持久化余额体现了价值的凭空创造或消失 | 输入验证拒绝了该请求;所谓的“差异”仅是显示格式问题 |
| 过期门控 / saga 缺口 | 已撤销的能力仍执行了持久化操作,或服务在流程中断/重复后状态不一致 | 短期缓存 TTL 有明确文档记录,且会自行关闭,不产生持久化影响 |
始终需要排除的两类误报:
- 真正的幂等操作。 强制执行的 ETag/
If-Match、DB UNIQUE 约束或INSERT ... ON CONFLICT DO NOTHING会将重复操作合并为一次提交——并行请求看起来都已被接受(全部为 200),但实际上只有一个生效。务必读取持久化状态,切勿只相信状态代码。 - 仅视觉层面的异常。 如果服务端状态正确,重复显示的提示、显示两项内容的乐观 UI,或重新获取数据前短暂显示的计数,都不构成发现。没有持久化状态变化,就没有发现——最多将其记录为加固建议,绝不能定为严重性问题。
未能在约定的请求上限内成功复现的候选问题应标记为 needs_validation,并附上 validation_plan(即在 staging 中运行的确切突发请求),而不是确认为发现。
输出格式
请使用共享的发现术语报告;有关 findings.json 记录以及如何验证/导出,请参见 references/findings-format.md。
# Business Logic & Race Condition Test Report
## Target / environment: [app + staging vs prod, test-mode confirmation]
## Authorization: [scope reference, request-count cap]
### Invariant register
| Invariant | Family | Enforced where | Violation proof |
### Findings
#### [verdict] [Title]
**Invariant violated:** [conservation / uniqueness / monotonicity / exclusivity]
**Endpoint(s):** `METHOD /path`
**Preconditions:** [accounts, state, concurrency needed]
**Reproduction:** [the controlled burst — count, sync method; or the step sequence]
**Durable-state proof:** [the balance/row/counter read back that shows the violation]
**Verdict:** confirmed | needs_validation (+ blockers, validation_plan) | rejected (+ ruled_out_by {control, location})
**Severity:** [only if confirmed — see severity-calibration.md]
### Hardening notes (not findings)
[Visual-only glitches, defense-in-depth gaps with no traced durable impact]
边界
- 未获得明确的书面授权,且授权中列明相关目标时,拒绝针对生产环境资金流、库存账本或正在运行的第三方计费 API执行并发或逻辑测试。默认使用预发布环境/测试模式。
- 拒绝超过约定的请求数量上限,或将概念验证变成持续负载测试——受控的双重扣款验证只需十几个请求,而不是压力测试(这是 DoS;请参阅
web-pentest的边界)。 - 绝不将第三方系统(支付提供商自己的 API、上游 SaaS)作为测试目标——默认情况下,这些系统不在范围内,即使范围内的应用会调用它们也不例外。
- 概念验证应使用能够证明违规行为的最小请求突发,而不是可重复用于持续提取数据的漏洞利用。
- 如果测试转移了真实资金或真实库存,立即停止并报告;不要通过发起会修改更多状态的补偿交易来“清理”。
交叉引用
owasp-auditA06/A02/A08 — 源码中的竞态模式:TOCTOU、原子队列认领(UPDATE ... WHERE status='pending' RETURNING)、先预留后操作、外部资源创建过程中的 TOCTOU。阅读代码,找到此技能要通过动态测试验证的检查后操作时间窗口。web-pentest第 8 阶段(业务逻辑)及其“白盒 + 黑盒组合模式”——当源码和运行中的实例均可用时,代码层面的发现有助于精确定位需要竞态测试的时间窗口。api-audit— 幂等性、BOLA,以及 API 层面的对象/功能授权。finding-triage— 根据具体情境确定每个候选发现的处置结论和严重性。
参考资料
- Strix(Apache-2.0)——
vulnerabilities/race_conditions.md和vulnerabilities/business_logic.md(https://github.com/usestrix/strix)。对技术内容进行了改述,未直接复制。本技能有意**不采用** Strix 的“绝不拒绝/绝不质疑授权”立场:此处会强制执行授权要求、优先使用预发布环境,并限制请求数量。 - PortSwigger Web Security Academy — 竞态条件(单数据包攻击)和业务逻辑实验。
- OWASP Web Security Testing Guide(WSTG)——业务逻辑测试章节。