Bug Hunting Journey

Writeups Race

Race

One coupon, thirty-seven redemptions, forty milliseconds

Severity Medium CVSS 6.5
Impact
Unlimited stacking of a single-use discount
Bounty
$500 - $1k
Program
Private

TL;DR

The cart service validated a coupon, called the billing service, then decremented the usage counter. Those two operations were not atomic: thirty-seven parallel requests consumed a single-use coupon thirty-seven times.

Race conditions are the one bug class where the proof fits in a chart. This one leaves a perfectly readable trace: thirty-seven 200s for a coupon that allowed one.

The window

The cart code, reconstructed from observed behaviour, does three things in this order: it reads the coupon, it calls the billing service, then it writes the new counter. The network call in the middle opens a window of several tens of milliseconds during which every concurrent request reads the same value.

The vulnerable patternJavaScript
const coupon = await db.coupon.findOne({ code });        // readif (coupon.uses >= coupon.maxUses) throw new Error('used');const invoice = await billing.apply(cart, coupon);       // ← the windowawait db.coupon.update({ code }, { uses: coupon.uses + 1 });

Confirmation

Forty requests sent in a single HTTP/2 packet, to remove network jitter between them:

Burst - HTTP/2 single packetbash
python3 race.py --target https://app.target.tld/api/cart/apply-coupon \  --concurrency 40 --single-packet \  --body '{"code":"WELCOME50"}' \  --cookie "session=████████████████"
200Aggregated resultresponse
HTTP/2 200 OK   × 37HTTP/2 409 Conflict × 3

Thirty-seven 50% discounts applied against a coupon flagged maxUses: 1. The final counter in the database read 1.

Takeaways

  • Any read → decide → write with a network call inside it is a race condition. The question is not whether the window exists, but whether anyone knows how to aim at it.
  • The fix is not an application-level lock but a conditional write: UPDATE … SET uses = uses + 1 WHERE code = ? AND uses < max_uses, then check the affected row count.
  • The final counter read 1. No business dashboard anywhere would have flagged the abuse.

Disclosure timeline

  1. Report sent
  2. Triaged
  3. Fix deployed conditional decrement in a single SQL statement
  4. Cleared for publication