race-condition

star 72

检测竞态条件风险;当目标存在限量资源操作(优惠券领取、积分兑换、余额操作、限量抢购)时触发;适用于并发请求可能绕过业务限制的场景。

Q16G By Q16G schedule Updated 6/7/2026

name: race-condition description: 竞态条件 / TOCTOU 检测 — 检测限量资源操作(优惠券领取、积分兑换、余额操作、限量抢购、状态切换)在并发请求下是否绕过业务限制;适用于检查-修改非原子的业务流程。 when-to-use: 当目标存在限量资源操作(优惠券领取、积分兑换、余额操作、限量抢购),并发请求可能绕过业务限制时 allowed-tools: bash,read_file,list_files,rg user-invocable: false

竞态条件 / TOCTOU 检测

成因引用

竞态条件成因:source(并发请求)→ sink(共享资源:库存 / 余额 / 限量优惠 / 数量字段 / 状态机)。"检查→执行"两步非原子,多并发请求都通过检查后才写入,业务命名不可作筛选——无论端点叫"优惠券领取"、"余额扣减"、"积分兑换"、"限量抢购"、"状态切换"、"一次性 token 使用",sink 语义都是"对共享有限资源做检查-修改",属同一范围。详见同根目录 pentest/web-security-testing/SKILL.md 漏洞成因图谱 · 竞态条件 / TOCTOU 行(不在本 skill 重复成因)。

关键 sink 形态:服务端在"读取当前状态 → 校验是否允许 → 写入新状态"三步中,未用原子操作(事务 + 行锁 / CAS / 唯一约束 / 分布式锁)保证,导致并发请求可在同一窗口都通过检查、都执行写入——产生超卖、超发、透支、状态错乱等真实业务损失。

触发线索(基线检查项)

以下是已知的常见竞态触发线索,作为基线起点而非必检硬清单。结合目标代码与上下文动态调整:

  • 适用且已完成 → 标注 [x] done
  • 明确不适用 → 标注 [-] n/a (原因),原因要具体到代码事实
  • 基线未列出但实际发现 → 新增条目并标注 [+] added (来源)

基线触发线索按"sink 语义"分类(不按业务命名):

  • 优惠券 / 红包 / 礼品码领取:一人限领一次 / 总量限制的领取入口
  • 限量抢购 / 秒杀 / 库存扣减:SKU 库存为整型字段、下单先 select 再 update 形态
  • 余额 / 积分扣减:钱包支付、积分兑换、虚拟资产消费
  • 一次性 token / 验证码核销:邀请码 / 激活码 / OTP 一次性使用
  • 状态机切换:订单 pending→paid / pending→cancel、审批 submit→approve 等单向状态切换
  • 投票 / 点赞 / 关注:一人一票、一次性点赞、关注数计数
  • 签到 / 任务领奖:每日仅一次的签到、任务完成领奖
  • 跨子系统覆盖:多个子系统各自有独立限量资源逻辑——每个子系统都需独立测
  • 代码模式:后端代码出现 if balance >= amount { balance -= amount } / if claimed == 0 { insert claim record } / SELECT stock FROM ... ; UPDATE stock = stock - 1 非事务 / 无行锁形态

思考检查点

加载本 skill 时按这些问题思考:

  • 这个端点是否对某共享有限资源做"读 → 判 → 写"?三步是否在同一事务、同一行锁内?
  • 事务隔离级别是 RC 还是 RR?应用层 SELECT + UPDATE 分两步是否仍有 TOCTOU?
  • 资源字段是否带唯一约束 / CHECK 约束 / 应用层 CAS?还是只靠应用层 if 判断?
  • 状态机切换是否依赖"当前状态 == X 才能切到 Y",且未用 UPDATE ... WHERE state=X 这类 CAS 形态?
  • 同一类 sink("共享资源 check-then-write")的其他端点是否也同模式?跨子系统是否独立测过?

前置条件与安全边界

  • 仅在授权环境测试;并发请求数控制在 5~20 个,不造成服务过载。
  • 测试后检查并清理异常数据(多领的优惠券、多扣的余额、错乱的状态等)。
  • 并发操作优先在自己持有的哨兵资源上进行(自有账号 / 自建优惠券 / 自建 SKU / 自有余额)。若竞态效果会不可逆地破坏共享真实数据(如真实库存超卖、他人数据被毁)且无法用哨兵承接,按 common/closure-verification.md 的《破坏性 / 不可逆动作的闭环边界》执行——改走非破坏差分,做不到就停 suspected,不得靠破坏真实业务数据来闭环。
  • 使用 HTTP/2 单一数据包攻击(Single Packet Attack)时注意服务器兼容性。

检测步骤

Step 1:目标操作识别

识别存在"检查 → 执行"两步模式的业务操作:

  1. 检查:验证前置条件(余额充足、未领取过、库存足够、状态为 pending
  2. 执行:实际写入(扣款、发放、减库存、切状态)
  3. 两步之间存在时间窗口 = 竞态条件机会

Step 2:基线采集

  1. 正常执行一次目标操作,记录请求和响应。
  2. 确认前置条件(如初始余额、库存数量、当前状态)。
  3. 确认操作后的状态变化(余额减少、库存减少、状态切换)。

Step 3:并发请求发送

使用以下策略发送并发请求:

  • 策略 1:简单并发 — 用并发工具发送 5~10 个相同请求,检查是否有多个请求都返回成功。
  • 策略 2:HTTP/2 单一数据包攻击 — 将多个请求封装在同一个 TCP 数据包中发送,消除网络延迟差异;这是最精确的竞态测试方法。
  • 策略 3:条件竞争边界 — 准备刚好满足条件的场景(余额 = 1 次消费金额、库存 = 1 件、邀请码剩 1 次),并发发送 2~5 个请求,检查是否出现透支 / 超卖 / 多核销。

并发操作须在自有哨兵资源上进行。若证明 confirmed 必然不可逆破坏真实/共享数据(如真实库存超卖、他人资源被毁),按 common/closure-verification.md《破坏性 / 不可逆动作的闭环边界》改走非破坏差分,做不到则降 suspected,不得靠破坏真实数据闭环。

Step 4:结果验证 + 跨端点 / 跨子系统传播

  1. 统计成功响应数量。
  2. 回读实际业务状态:
    • 余额是否为负(透支)
    • 优惠券是否多领
    • 库存是否为负 / 超出总量
    • 状态机是否到达不可达组合 / 同一记录被双重切换
  3. 与预期单次执行结果对比,排除幂等导致的"假成功"。
  4. 若该端点确认存在竞态 → 按"端点矩阵传播"横扫同子系统其他限量资源 + 跨子系统的同类业务。

示例库

正例形态(代码层根因)

  • 领取优惠券:SELECT count FROM claim WHERE user_id=? AND coupon_id=? → if 0 → INSERT claim ...,两步无事务、claim 表无 UNIQUE(user_id, coupon_id) 约束,可并发多领(coupon-claim-check-then-insert-race
  • 余额扣减:SELECT balance FROM wallet WHERE uid=? → if balance >= amount → UPDATE wallet SET balance=balance-? WHERE uid=?,应用层判断 + 后续 update,可并发透支至负数(balance-deduct-select-then-update-race
  • 库存扣减:SELECT stock FROM sku WHERE id=? → if stock > 0 → UPDATE sku SET stock=stock-1,未使用 UPDATE ... WHERE stock > 0 这类原子条件,并发可超卖(stock-decrement-non-atomic-oversell-race
  • 状态机切换:SELECT status FROM order WHERE id=? → if status='pending' → UPDATE order SET status='paid',未使用 UPDATE ... WHERE status='pending',可被并发两次切换(state-transition-non-atomic-double-spend-race

窄化反例(必须避免)

以下是竞态维度的典型窄化误判:

  • "看起来有事务 → 安全" — 错。事务隔离级别(RC vs RR)下,应用层 SELECT + UPDATE 仍可能在两条独立 SQL 之间观察到旧值;只有 SELECT ... FOR UPDATEUPDATE ... WHERE <条件> 这类持锁 / 原子 CAS 才真正消除窗口。
  • "已测了某接口 → 跳过其他限量资源" — 错。每种限量资源(优惠券 / 余额 / 库存 / 状态机 / 一次性 token / 投票 / 签到)都是独立 sink,必须按端点账本逐个测。
  • "限量字段是 int → 不会负数" — 错。Go / Java 等有符号 int 会在边界回绕;DB 字段定义为 unsigned 也可能在应用层被先减再写入,应用层把负值当成 0 处理也会形成"看似无负数实则超发"。
  • "已在子系统 A 命中竞态 → B 假定同样" — 错。跨子系统的限量资源逻辑独立实现,必须按子系统独立结账。
  • "看起来并发量小 → 不会触发" — 错。HTTP/2 单连接多路复用、攻击者本地多协程、Single Packet Attack 都能轻松构造高密集并发;竞态不取决于"业务真实并发量",只取决于服务端是否原子。

反例义务(必须遵守)

为什么这里是「必须」:反例义务属于交付契约——"该子系统竞态已防护"结论是覆盖完整性的产物声明,缺失反向验证清单会让下游误信"该维度全站安全"。

写"未发现竞态 / TOCTOU"或"已防护"前,产物必须包含:

  • 测过的竞态候选端点完整清单(按 sink 语义枚举:所有"对共享有限资源做检查-修改"的端点;含优惠券领取 / 余额扣减 / 库存扣减 / 状态机切换 / 一次性 token / 投票 / 签到等全部触发线索类别)
  • 每个端点测过的并发策略(简单并发 / HTTP/2 Single Packet / 条件竞争边界)
  • 每个端点的回读状态证据(基线状态 / 攻击后状态 / 差异点 / 是否原子)

清单不完整 → 结论降级为 partial-coverage 并显式声明未覆盖范围。

特别警示:只测了"优惠券领取"而未覆盖"余额扣减 / 库存 / 状态机切换"等其他限量资源,不能下"全站无竞态"结论。

闭环验证要求(必须遵守)

通用闭环口径见同根目录 common/closure-verification.md(技能表 path 列同一抽取根下,需要时 read_file 读取)。核心:结论须形成「输入 → 处理 → 真实危害 → 可复核证据」完整证据链;仅凭"多个请求返回 200"等中间信号最多判 suspected,证明实际业务状态发生了超出预期的变化(余额负值 / 多领 / 超卖 / 状态错乱)才判 confirmed。本漏洞特有要点:

  • 仅凭"多个请求返回 200"不得判定 confirmed,必须回读业务状态确认超出预期的变化。
  • 需排除"幂等操作"(即使多次请求成功,但只有一次生效)——通过差分基线状态与攻击后状态确认有效写入次数。
  • 不可逆动作例外:若 confirmed 必须靠不可逆破坏真实 / 共享数据才能证明,按 common/closure-verification.md《破坏性 / 不可逆动作的闭环边界》——优先自有哨兵资源或非破坏差分,做不到则降 suspected

判定标准

现象 判定
并发请求导致业务状态超出预期(余额透支、多领、超卖、状态错乱),通过回读确认 confirmed
多个请求返回成功但无法确认实际业务影响(如无法回读状态 / 区分幂等) suspected
并发请求仅有一个成功或业务状态正常(正确的原子操作) not vulnerable
只测了部分子系统 / 部分限量资源 / 部分并发策略 partial-coverage(不得宣称 safe)

上表 confirmed 若需不可逆破坏真实/共享数据才能证明,按 common/closure-verification.md《破坏性 / 不可逆动作的闭环边界》——优先自有哨兵或非破坏差分,做不到则降 suspected

修复建议

  • 使用数据库事务 + 行锁(SELECT ... FOR UPDATE)保证检查与写入原子。
  • 使用乐观锁(版本号 / CAS):UPDATE ... SET stock=stock-1 WHERE id=? AND stock>0 这类原子条件。
  • 对关键操作使用分布式锁(Redis SETNX / Redlock / Zookeeper)。
  • 使用数据库唯一约束(如 UNIQUE(user_id, coupon_id))防止重复领取。
  • 状态机切换使用 CAS:UPDATE ... SET status='paid' WHERE id=? AND status='pending' 并检查 affected rows。
  • 对计数 / 余额字段使用 UPDATE ... SET balance=balance-? WHERE balance>=?,避免应用层判断后写入。
Install via CLI
npx skills add https://github.com/Q16G/aster --skill race-condition
Repository Details
star Stars 72
call_split Forks 6
navigation Branch main
article Path SKILL.md
More from Creator