Quick answer
Marketplace race conditions are the most profitable scenario of 2026 — devs think about SQL transactions but not about simultaneous HTTP. The rule: any endpoint that reads state, decides, and then writes is vulnerable if the window between read and write exceeds 1ms. Patterns: coupon applied N times (non-atomic claim/use), stock sold beyond inventory (non-atomic decrement), double checkout (transfer starts before locking), refund applied multiple times. Tooling: Turbo Intruder with single-packet HTTP/2 synchronizes requests to <1ms. Typical bounties: $1,500-$15,000.
Why races happen
The web is transactional, databases are transactional — but the code in between rarely is. A typical "use coupon" endpoint:
app.post("/coupon/use", async (req, res) => {
const coupon = await db.coupon.findOne({ code: req.body.code });
if (!coupon || coupon.used) return res.status(400).send("Invalid");
await db.coupon.updateOne({ code: req.body.code }, { used: true });
await db.cart.applyDiscount(req.user.cart, coupon.amount);
res.send("OK");
});
The bug: between findOne (line 2) and updateOne (line 4) there's a window of ~5-50ms. If two requests arrive in that window, both see used: false, both pass the validation, both apply the discount.
The problem space
| Code pattern | Race window | Exploitation probability |
|---|---|---|
| Read → decide → write (same process) | 1-10ms | High |
| Cache check → DB write | 10-100ms | Very high |
| Microservices (call A → call B) | 50-500ms | Almost guaranteed |
| Webhook + state update | 100ms-seconds | Almost guaranteed |
Tooling — single-packet attack
Turbo Intruder (Burp extension) implements the single-packet attack documented by James Kettle (2023): it packs N requests into a single TCP/HTTP-2 packet, arriving at the server simultaneously. It beats:
- Network jitter (variable latency between requests)
- Per-IP rate limits that count per second
- Small mutexes (<10ms)
Base script
def queueRequests(target, wordlists):
engine = RequestEngine(
endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2
)
for i in range(20):
engine.queue(target.req, gate="race1")
engine.openGate("race1")
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)
gate="race1" makes the 20 requests prepare but wait — then openGate releases them all at once with a single packet.
<!-- PAYWALL -->[!info] HTTP/2 advantage HTTP/2 allows multiple streams on one TCP connection. The final frame of the headers of N requests fits in a single IP packet → they reach the server microseconds apart, not milliseconds.
Pattern 1 — coupon applied N times
Marketplace with single-use coupons. Endpoint POST /api/coupons/apply with {"code": "SUMMER20"} applies a 20% discount.
Test
- Create a cart of 100€.
- Capture the apply-coupon request in Burp Repeater.
- Send to Turbo Intruder, fire 20 simultaneous with a gate.
- Refresh the cart: do you have 20% × 20 = 400% discount → negative price? Bug.
Variants
- Discount stack: applying the same code N times stacks it arithmetically.
- Code consumption: the code is marked
usedbut theappliedpropagates to the cart before the mark. - Different simultaneous carts: apply the same coupon to N parallel carts before it's marked used.
Impact framing
- Discount > 100% → refund above the price → "store credit" extracted.
- Coupon "first order $50 off" used 50 times → $2500 stolen.
Real bounty: H1 booking program, $8,400 — combination with HTTP/2 single-packet bypassing an anti-bot rate limit.
Pattern 2 — negative stock
The "add to cart" / "reserve product" endpoint decrements stock but the typical flow:
const product = await db.product.findOne({ id });
if (product.stock < requested) return error;
await db.product.updateOne({ id }, { $inc: { stock: -requested } });
20 simultaneous requests, each asking for the last item in stock → all see stock: 1 → all pass the validation → 20 reservations for 1 available unit.
Profitable patterns
- Limited-edition drop (sneakers, NFT-like collectibles): reserve 20 items of the only available one → resale or demand delivery of 20.
- Subscription with free trial: race between "check if user had trial" and "mark trial as used" → N trials.
- Single-use discount code per user: the same, applied per user.
Quick detection
Can the product go to stock: -19? Verify with a quick scan via the admin API (if you have test access) or with the product detail page after the race.
Pattern 3 — double checkout
The POST /api/checkout/finalize endpoint runs:
1. Lee balance del user
2. Lee total del carrito
3. Charge a la tarjeta (call externo a Stripe)
4. Mark carrito as paid
5. Emit order
The window between step 1 (reads balance) and step 4 (mark paid) is huge because step 3 takes 500ms-2s (external network). Race attack:
T=0 request1 → step 1: balance 100€
T=10ms request2 → step 1: balance 100€ (no actualizado todavía)
T=500ms request1 → step 4: paid + balance 0€
T=500ms request2 → step 4: paid pero el balance fue de 100€ cuando empezó!
Result: two paid orders but only one was charged. Variant: charge 100€ × 2 to the card but you only receive 1 order → the attacker requests a refund for 1 and keeps the other.
The inverted attack: refund race
The /api/orders/X/refund endpoint can run N times before marking the order as refunded:
T=0 request1: refund 100€ → balance += 100
T=5ms request2: refund 100€ → balance += 100 (order todavía no marcada como refunded)
T=10ms request3: refund 100€ → balance += 100
...
Original payment 100€, refunds N × 100€ → the attacker's balance goes from 0 to 1000€ with 10 races.
Pattern 4 — referral code / signup bonus
The signup endpoint checks whether the referral code is used by this IP/device:
1. Validate referral code
2. Create user
3. Credit referrer with bonus
4. Credit new user with bonus
5. Mark referral as redeemed (vinculado a user nuevo)
Race on step 1: 20 simultaneous signups with the same referral → 20 new users created → 20× credits to the referrer.
Marketplace with a €5 bonus per referral → race 20 → €100 stolen. Scaled with sock puppets → thousands.
Pattern 5 — KYC / verification status
Apps with KYC require verification before high-value operations (transfer >1000€). If the endpoint:
POST /api/account/verify-kyc → marca user como verified
is rate-limited but not race-protected, and there's an endpoint:
POST /api/transfer/large → require user.kycVerified
A race between verify (which passes or doesn't) and the transfer can slip the transfer through while KYC is processed async. A less common bug but high severity in fintech.
How to distinguish a real race from "the server queues them"
Some servers serialize requests to the same resource (optimistic lock, mutex). Identify:
# Tras race attack, observa los status codes en orden
[200, 200, 409, 409, 409, 200, ...] ← parcialmente protegido
[200, 200, 200, 200, 200, 200, ...] ← race exitoso (todos pasan)
[200, 409, 409, 409, ...] ← solo 1 pasa, los demás rechazados → bien protegido
If they all respond 200 but the final state reflects only 1 operation → successful race but later deduplication → the bug may exist with reduced impact. Investigate the final state (DB query, balance check).
Improving the hit probability
- HTTP/2 vs HTTP/1.1: H2 synchronizes better (single packet with many streams).
- Geographic proximity: use a server close to the target. If the target is in us-east, launch from us-east. Reduces jitter.
- Pre-warmed connection: turbo-intruder keeps connections open → the TCP handshake doesn't count toward the race.
- Repeat & average: no race attack hits 100% — repeat 5-10 times, observe the maximum parallelism achieved.
[!warning] Test responsibly Race exploits can corrupt state in production. Test in the program's sandbox if it exists. If not, limit races to 5-10 parallel (not 100), work on your own account, and warn before testing.
Reporting
- Severity factor: financial (refund/discount/transfer) → typically Critical. Non-monetary logic flaw (free trial abuse, vote stuffing) → High/Medium.
- Minimal PoC: a video recording the cart before (normal price) → cart after (negative price). No reason to extract N × real money.
- Suggested fix: row-level lock (
SELECT ... FOR UPDATE), unique constraint on (couponId, userId), idempotency keys, transactionalfindAndModifywith the check included.
Hunting checklist
- Map endpoints that touch money/stock/permissions/credits.
- For each endpoint: does it read state, decide, and then write? Window = race surface.
- Capture the request in Burp → Turbo Intruder → single-packet H2 with a gate.
- Coupon apply: race 20 parallel → discount > 100% or applied N times?
- Stock: race 20 parallel on the last item → negative stock?
- Checkout: race 5 parallel on the same cart → single payment but multiple orders?
- Refund: race 10 parallel → balance += 10 × price?
- Referral signup: race with the same code → N referrals for 1?
- KYC / verification status: race between verify and a high-value operation.
- Confirm with a scan of the final state (DB / API) — 200 status codes don't guarantee they passed.
- Report with a minimal PoC (5-10 races max) + suggested fix (row-level lock).
Related labs
Practice single-packet H2, coupon stacking, negative stock and double checkout in race conditions labs.
Practice this in a lab
Race Conditions Marketplace
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
There's an extra payload at the end
El single-packet attack con HTTP/2 que vence rate limits y races contra mutex pequeños, ganando $8400 bounty en marketplace de bookings.
€7.99/mo · cancel anytime
Related articles
0-click Account Takeover — OTP brute force + Email Normalization
Two separate flaws look minor. Together, they hand you full ATO knowing only the email. Real bounty: €560 and 12 minutes of exploitation.
File Upload — extension, content-type and magic bytes bypasses
10 bypasses to upload webshells: double extension, null byte, content-type spoof, magic bytes, polyglots, race conditions and path traversal abuse.
JWT — vulnerabilities, bypasses and claim manipulation
alg=none, RS256→HS256 confusion, kid SQLi/path traversal, jku spoofing, secret cracking with hashcat. How to hunt poorly verified JWTs.