Skip to content

【安全】兑换码可被并发重复兑换(仅影响 MySQL) #2398

@sec-reex

Description

@sec-reex

【安全】兑换码可被并发重复兑换(仅影响 MySQL)

漏洞描述

摘要

兑换码充值接口(POST /api/user/topup)存在竞争条件(Race Condition)漏洞,导致同一个一次性兑换码可被多个用户账户同时重复兑换,从而实现无限制余额充值。


受影响版本

所有版本:

  • v0.1.6-alpha(2023-04-26)
  • v0.6.11-preview.7(2026-03-04,最新版本)

影响范围:

  • 仅影响 MySQL 后端
  • SQLite 不受影响

根本原因

代码通过数据库事务中的 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

两个测试账户均只有初始额度。

截图:

Image

2. 攻击前兑换码列表

在执行 PoC 前,兑换码管理面板为空。

随后 PoC 创建一个一次性测试兑换码用于并发兑换测试。

截图:

Image

3. PoC 并发兑换结果

PoC 同时使用两个普通用户账户请求:

POST /api/user/topup

两个账户均成功使用同一个一次性兑换码,并同时收到:

{
  "success": true
}

HTTP 状态码:

HTTP 200

PoC 可以稳定复现漏洞,说明兑换流程不存在有效的并发保护。

截图:

Image

4. 兑换码状态被标记为 Used

攻击完成后,后台管理面板显示:

Used

兑换码额度为:

$0.20

然而:

  • 系统仅记录了一次兑换
  • 两个账户却均获得了完整额度

说明同一个一次性兑换码被并发消费了两次。

截图:

Image ---

5. 攻击后用户额度翻倍

攻击结束后:

  • testuser1$0.20 → $0.40
  • testuser2$0.20 → $0.40

即:

  • 一个价值 $0.20 的一次性兑换码
  • 被两个账户同时完整消费
  • 系统总共发放了 $0.40 的额度

截图:

Image

漏洞影响

攻击者仅需:

  • 两个及以上普通用户账户
  • 一个有效兑换码

即可实现:

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

该写法会正确生成事务内的:

SELECT ... FOR UPDATE

从而确保行级锁真正生效。

时间线

日期 事件
2023-04-26 漏洞引入(v0.1.6-alpha)
2023-07-07 第一次部分修复 —— 事务连接问题
2023-07-23 第二次部分修复 —— 引入 gorm:query_option 错误
2026-03-04 最新版本仍然存在漏洞

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions