What Is a Race Condition Vulnerability?
A race condition vulnerability in a web application occurs when the application's security or business logic assumptions break down under concurrent execution. The application assumes operations happen in a safe, sequential order — but when two or more requests arrive simultaneously, the actual execution interleaves in ways the developer didn't account for, producing incorrect or exploitable results.
Race conditions are among the most underappreciated and underreported vulnerability classes in web security. They're hard to find with standard scanners, don't leave the obvious fingerprints of injection vulnerabilities, and often require precise timing to trigger reliably. But their impact can be severe: free money from digital wallets, unlimited use of single-use discount codes, authentication bypass, and privilege escalation have all been achieved through race condition exploitation.
OWASP classifies race conditions under CWE-362 (Concurrent Execution Using Shared Resource with Improper Synchronization) and they appear in the broader OWASP Top 10 under A04:2021 Insecure Design.
How Race Conditions Happen
The Conceptual Model
Consider a simple gift card redemption endpoint. The intended flow:
``` 1. Check: Has this gift card already been used? → No 2. Mark the card as used 3. Credit the account balance ```
This is safe if executed sequentially — the card is marked used before the balance is credited. But what happens when two requests arrive at the same millisecond?
``` Thread A: Thread B: 1. Check card → Not used 1. Check card → Not used 2. Mark as used 3. Credit balance 2. Mark as used 3. Credit balance ```
Both threads read the card as "not used" before either has written the "used" status. The card is redeemed twice. The account receives double the credit.
This class of bug is called TOCTOU — **Time Of Check To Time Of Use**. The state is checked at one moment, used at another moment, and an attacker can manipulate what happens in between.
The Web Application Context
In web applications, race conditions typically manifest as:
- **Limit bypass** — one-time discount codes redeemed multiple times, daily withdrawal limits exceeded, free tier feature limits bypassed - **Balance manipulation** — cryptocurrency or digital wallet double-spends - **Authentication bypass** — two-factor authentication codes consumed before validation completes - **Privilege escalation** — role changes processed in a window where old permissions persist - **Inventory oversell** — purchasing more of a limited-quantity item than stock allows
Classic Web Race Condition Exploits
Coupon Code Exhaustion
A promotional discount code is marked "single use" in the database. The check-and-update operation isn't atomic:
```python # VULNERABLE def apply_coupon(code, user_id): coupon = db.query("SELECT * FROM coupons WHERE code = %s", code) if coupon.used: return "Coupon already used" # Race window here — another thread can reach this point simultaneously db.execute("UPDATE coupons SET used = true WHERE code = %s", code) db.execute("UPDATE orders SET discount = %s WHERE user_id = %s", coupon.discount, user_id) return "Discount applied" ```
An attacker sends 20 simultaneous POST requests with the same coupon code. Several threads pass the `coupon.used` check before any thread has written `used = true`. Multiple discounts are applied to different orders.
Digital Wallet Double-Spend
```python # VULNERABLE def withdraw(user_id, amount): balance = db.query("SELECT balance FROM wallets WHERE user_id = %s", user_id) if balance < amount: return "Insufficient funds" # Race window db.execute("UPDATE wallets SET balance = balance - %s WHERE user_id = %s", amount, user_id) initiate_transfer(amount) ```
Balance: £100. Attacker initiates two simultaneous £100 withdrawals. Both threads read £100 as the balance, both pass the check, both deduct £100. Account goes to -£100. Two £100 transfers are initiated.
This exact pattern has been exploited against cryptocurrency exchanges and digital payment platforms with real financial impact.
Referral Bonus Abuse
Referral systems that credit a bonus when a referred user completes their first purchase are frequently vulnerable:
``` 1. Referred user completes purchase 2. System checks: has referral bonus been paid for this referral? → No 3. System pays the bonus ```
If a referred user can trigger multiple simultaneous "first purchase" requests (by rapidly clicking a payment confirmation button, or through automated request duplication), multiple bonus payments may be triggered for the same referral event.
Rate Limit Bypass
Login attempt rate limiting implemented without atomic counter operations:
```python # VULNERABLE def check_rate_limit(ip): attempts = cache.get(f"attempts:{ip}") or 0 if attempts >= 10: return False cache.set(f"attempts:{ip}", attempts + 1, ttl=300) return True ```
Multiple simultaneous requests all read `attempts = 0`, all pass the check, all set `attempts = 1`. The rate limiter sees 1 attempt; 50 parallel requests have been allowed through.
Advanced Exploitation: Last-Byte Synchronisation
Modern race condition exploitation against web applications is sophisticated. The primary challenge is achieving simultaneous arrival of multiple requests at the server — network jitter means requests sent at the "same time" from the attacker's perspective arrive milliseconds apart, which may be enough for the server to process them sequentially.
**Last-byte synchronisation** (popularised by PortSwigger researcher James Kettle) addresses this by sending all but the final byte of each request, then simultaneously sending the last byte of all requests:
``` Connection 1: POST /redeem... [body minus last byte] → wait Connection 2: POST /redeem... [body minus last byte] → wait Connection 3: POST /redeem... [body minus last byte] → wait
Simultaneously send last byte on all connections → requests complete simultaneously ```
HTTP/2's single-connection multiplexing makes this even more precise — multiple requests can be sent in a single TCP frame, eliminating network jitter entirely. Tools like Turbo Intruder (Burp Suite extension) implement this technique.
Detecting Race Condition Vulnerabilities
Race conditions are difficult to detect with traditional scanning because:
- They require concurrent requests, not sequential probing - They may require precise timing - The exploitable window can be microseconds - Standard test cases don't typically send parallel requests
Manual testing approaches:
**Identify limit-enforcement endpoints** — any endpoint that enforces a limit (one use, maximum balance, rate limit, stock quantity) is a candidate. Test by sending 10–50 simultaneous requests with Turbo Intruder or a similar tool.
**Look for non-atomic read-modify-write patterns** — anywhere the application reads a value, makes a decision, then writes an update is a TOCTOU candidate. Review the code or infer from application behaviour.
**Time the operation** — operations with observable delay between the check and the update have larger race windows. A 50ms processing time is a large window compared to a 1ms operation.
Prevention
Atomic Database Operations
The most reliable prevention is making the check and the update a single atomic database operation:
```sql -- WRONG: two separate operations SELECT used FROM coupons WHERE code = 'ABC123'; UPDATE coupons SET used = true WHERE code = 'ABC123';
-- RIGHT: single atomic update with the check embedded UPDATE coupons SET used = true WHERE code = 'ABC123' AND used = false; -- Check rows affected: if 0, coupon was already used ```
With a single UPDATE statement, the database's internal locking ensures only one transaction can successfully claim the row with `used = false`. Concurrent transactions queue behind the lock. The first succeeds; all others update 0 rows.
Database Transactions with Appropriate Isolation
When a single atomic statement isn't sufficient, wrap the operation in a database transaction with the correct isolation level:
```python with db.transaction(isolation_level='SERIALIZABLE'): balance = db.query("SELECT balance FROM wallets WHERE user_id = %s FOR UPDATE", user_id) if balance < amount: raise InsufficientFunds db.execute("UPDATE wallets SET balance = balance - %s WHERE user_id = %s", amount, user_id) ```
`FOR UPDATE` acquires a row-level lock; other transactions attempting to read the same row for update will block until this transaction completes.
Distributed Locks for Non-Database Resources
When the operation isn't purely database-bound (it involves an external API call, a message queue, or a file operation), use a distributed lock (Redis SETNX, database advisory locks) to enforce mutual exclusion:
```python lock_key = f"redeem:{coupon_code}" if not redis.set(lock_key, "1", nx=True, ex=30): return "Try again" # Another process holds the lock try: # Safe to proceed — we hold the lock apply_coupon_logic() finally: redis.delete(lock_key) ```
Idempotency Keys
For financial operations, implement idempotency keys: a unique client-generated ID included with each request. The server stores processed idempotency keys; duplicate requests with the same key return the original result without reprocessing.
Yrzo AI's testing includes targeted race condition detection using parallel request techniques against limit-enforcement, balance management, and single-use token endpoints.
**[Test your application for race conditions → Start free at yrzoai.dev](https://yrzoai.dev)**
Twenty simultaneous requests. One coupon code. Multiple discounts applied. Race conditions are silent and devastating — find them before they cost you.
Find out if your website has these vulnerabilities
Yrzo AI runs 44 automated security checks and delivers a full report in under 20 minutes. Starting from £399.
Scan your website →