【安全】兑换码可被并发重复兑换(仅影响 MySQL)
漏洞描述
摘要
兑换码充值接口(POST /api/user/topup)存在竞争条件(Race Condition)漏洞,导致同一个一次性兑换码可被多个用户账户同时重复兑换,从而实现无限制余额充值。
受影响版本
所有版本:
- 从
v0.1.6-alpha(2023-04-26)
- 到
v0.6.11-preview.7(2026-03-04,最新版本)
影响范围:
根本原因
代码通过数据库事务中的 SELECT ... FOR UPDATE 行级锁来保护兑换流程。
然而,实现中使用了 GORM v2 的:
Set("gorm:query_option", "FOR UPDATE")
但该 key 在 GORM v2 中不会被任何代码处理。
因此,FOR UPDATE 子句实际上并未添加到 SQL 语句中,导致行锁从未真正生效。
当前代码(v0.6.11-preview.7):
// 当前实现 —— FOR UPDATE 被静默丢弃
err := tx.Set("gorm:query_option", "FOR UPDATE").
Where(keyCol+" = ?", key).
First(redemption).Error
实际执行的 SQL:
SELECT * FROM `redemptions`
WHERE key = ?
LIMIT 1
开发者预期的 SQL:
SELECT * FROM `redemptions`
WHERE key = ?
LIMIT 1
FOR UPDATE
由于未获取行锁,多个并发事务可同时读取到同一兑换码仍处于“未使用”状态,最终导致重复兑换。
漏洞复现与证据
1. 攻击前用户额度
攻击开始前:
testuser1:$0.20
testuser2:$0.20
两个测试账户均只有初始额度。
截图:
2. 攻击前兑换码列表
在执行 PoC 前,兑换码管理面板为空。
随后 PoC 创建一个一次性测试兑换码用于并发兑换测试。
截图:
3. PoC 并发兑换结果
PoC 同时使用两个普通用户账户请求:
两个账户均成功使用同一个一次性兑换码,并同时收到:
HTTP 状态码:
PoC 可以稳定复现漏洞,说明兑换流程不存在有效的并发保护。
截图:
4. 兑换码状态被标记为 Used
攻击完成后,后台管理面板显示:
兑换码额度为:
然而:
说明同一个一次性兑换码被并发消费了两次。
截图:

---
5. 攻击后用户额度翻倍
攻击结束后:
testuser1:$0.20 → $0.40
testuser2:$0.20 → $0.40
即:
- 一个价值
$0.20 的一次性兑换码
- 被两个账户同时完整消费
- 系统总共发放了
$0.40 的额度
截图:
漏洞影响
攻击者仅需:
即可实现:
1. 低成本倍增充值额度
一个 $0.10 的兑换码,被两个账户同时兑换后,可产生 $0.20 的总额度。
2. 攻击可无限扩展
无需特殊权限。
攻击者可自由注册多个账户并发兑换。
3. 造成直接经济损失
平台额度对应真实的上游大模型 API 成本。因此攻击会直接消耗平台付费资源。
4. 可稳定自动化利用
漏洞利用无需:
仅需普通用户账户即可完成。
修复方案
将:
tx.Set("gorm:query_option", "FOR UPDATE")
替换为 GORM v2 官方推荐的行锁写法:
import "gorm.io/gorm/clause"
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where(keyCol+" = ?", key).
First(redemption).Error
该写法会正确生成事务内的:
从而确保行级锁真正生效。
时间线
| 日期 |
事件 |
| 2023-04-26 |
漏洞引入(v0.1.6-alpha) |
| 2023-07-07 |
第一次部分修复 —— 事务连接问题 |
| 2023-07-23 |
第二次部分修复 —— 引入 gorm:query_option 错误 |
| 2026-03-04 |
最新版本仍然存在漏洞 |
【安全】兑换码可被并发重复兑换(仅影响 MySQL)
漏洞描述
摘要
兑换码充值接口(
POST /api/user/topup)存在竞争条件(Race Condition)漏洞,导致同一个一次性兑换码可被多个用户账户同时重复兑换,从而实现无限制余额充值。受影响版本
所有版本:
v0.1.6-alpha(2023-04-26)v0.6.11-preview.7(2026-03-04,最新版本)影响范围:
根本原因
代码通过数据库事务中的
SELECT ... FOR UPDATE行级锁来保护兑换流程。然而,实现中使用了 GORM v2 的:
但该 key 在 GORM v2 中不会被任何代码处理。
因此,
FOR UPDATE子句实际上并未添加到 SQL 语句中,导致行锁从未真正生效。当前代码(
v0.6.11-preview.7):实际执行的 SQL:
开发者预期的 SQL:
由于未获取行锁,多个并发事务可同时读取到同一兑换码仍处于“未使用”状态,最终导致重复兑换。
漏洞复现与证据
1. 攻击前用户额度
攻击开始前:
testuser1:$0.20testuser2:$0.20两个测试账户均只有初始额度。
截图:
2. 攻击前兑换码列表
在执行 PoC 前,兑换码管理面板为空。
随后 PoC 创建一个一次性测试兑换码用于并发兑换测试。
截图:
3. PoC 并发兑换结果
PoC 同时使用两个普通用户账户请求:
两个账户均成功使用同一个一次性兑换码,并同时收到:
{ "success": true }HTTP 状态码:
PoC 可以稳定复现漏洞,说明兑换流程不存在有效的并发保护。
截图:
4. 兑换码状态被标记为 Used
攻击完成后,后台管理面板显示:
兑换码额度为:
然而:
说明同一个一次性兑换码被并发消费了两次。
截图:
5. 攻击后用户额度翻倍
攻击结束后:
testuser1:$0.20 → $0.40testuser2:$0.20 → $0.40即:
$0.20的一次性兑换码$0.40的额度截图:
漏洞影响
攻击者仅需:
即可实现:
1. 低成本倍增充值额度
一个
$0.10的兑换码,被两个账户同时兑换后,可产生$0.20的总额度。2. 攻击可无限扩展
无需特殊权限。
攻击者可自由注册多个账户并发兑换。
3. 造成直接经济损失
平台额度对应真实的上游大模型 API 成本。因此攻击会直接消耗平台付费资源。
4. 可稳定自动化利用
漏洞利用无需:
仅需普通用户账户即可完成。
修复方案
将:
替换为 GORM v2 官方推荐的行锁写法:
该写法会正确生成事务内的:
从而确保行级锁真正生效。
时间线
gorm:query_option错误