Race conditions (CWE-362) exploit timing gaps between check and action — coupon reuse, balance manipulation, and limit overflows via single-packet HTTP/2.
TL;DR
SELECT FOR UPDATE) + Redis SETNX idempotency keysA race condition (CWE-362) occurs when a system's security outcome depends on the timing or interleaving of concurrent events that are not coordinated. In web security, this consistently manifests as a Time-of-Check to Time-of-Use (TOCTOU) sequence (CWE-367): the application reads a value to make a security decision, then acts on that decision in a separate non-atomic step. An attacker sends multiple concurrent requests to execute the "use" step before any one request's "check" result has been committed back to the database.
The vulnerability class was redefined by James Kettle at Black Hat USA 2023 ("Smashing the State Machine"). Before 2023, race conditions in web applications were considered impractical to exploit reliably over remote networks because 15–30ms of intercontinental jitter caused concurrent requests to serialize at the server. The single-packet attack — sending all HTTP/2 requests in a single TCP packet via last-byte synchronization — eliminated this barrier. GMO Flatt Security extended this further at CODE BLUE 2024 with first sequence sync, achieving up to 10,000 concurrent requests in 166ms via IP fragmentation.
Race conditions fall under OWASP A04:2021 (Insecure Design) because the root cause is a state machine design flaw: the application never guarantees that a check-and-act sequence is atomic. Unlike injection flaws, race conditions produce no malformed input. The attack consists entirely of legitimate, well-formed requests — just concurrent ones. This also explains why automated scanners miss them: there is no malicious payload to detect in a single request.
The core of every web race condition is a non-atomic check-and-act sequence. The database holds the authoritative state, but the application reads it into memory, reasons about it, then writes back — creating a window where concurrent reads all see the pre-update state.
The attack proceeds in four steps:
A functional PoC using httpx:
import asyncio
import httpx
async def race_coupon(url: str, headers: dict, body: dict, n: int = 20):
"""Race a coupon redemption endpoint N times concurrently."""
async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:
# Connection warming — TLS handshake + HTTP/2 SETTINGS frame
await client.get(url.rsplit("/", 1)[0] + "/", headers=headers)
# Fire N requests over same HTTP/2 connection
tasks = [client.post(url, headers=headers, json=body) for _ in range(n)]
responses = await asyncio.gather(*tasks, return_exceptions=True)
success = [r for r in responses if not isinstance(r, Exception) and r.status_code == 200]
return {"total": n, "success_count": len(success), "race_confirmed": len(success) > 1}| Variant | CWE | Technique | Impact | Bounty Range |
|---|---|---|---|---|
| TOCTOU (file/DB) | CWE-367 | Concurrent check-and-use on non-atomic read-modify-write | Privilege escalation, RCE | $1K–$50K |
| Limit-Overrun | CWE-362 | N concurrent requests past usage limit before counter commits | Coupon multi-redeem, free credits | $500–$10K |
| Balance Drain | CWE-362 | N concurrent withdrawals before any balance UPDATE commits | Negative balance, fund theft | $5K–$50K |
| Multi-Endpoint | CWE-841 | Race across two dependent endpoints in a state machine | Auth bypass, state corruption | $2K–$20K |
| Partial Construction (JIT) | CWE-362 | Exploit object during uninitialized window | Account takeover, privilege escalation | $2K–$15K |
| OAuth/JWT Race | CWE-362 | Authorization code double-redemption, refresh token rotation | Persistent access after revocation | $2K–$20K |
| File Upload Race | CWE-367 | Request execution during upload-validation gap | Webshell, RCE (Tomcat CVE-2024-50379) | $1K–$10K |
| CDN Cache Race | CWE-362 | Concurrent request during cache revalidation window | Cache poisoning (CVE-2025-32421) | $1K–$5K |
Limit-overrun is the most common bounty-yielding race. The application checks a counter (uses < max_uses, balance >= amount, already_voted == false), passes the check, then increments/decrements in a separate operation. Twenty concurrent requests all read the pre-update value and all pass.
JIT inconsistency is the subtlest category. Kettle documented Rails+Devise's partial construction window: a user row is inserted with confirmation_token=NULL before a background job populates the token, allowing confirmation with an empty token during that window. A default-permission window exists when a new account starts with admin privileges before a privilege-downgrade step completes.
OAuth/JWT races produce spare valid token pairs. Concurrent authorization code exchange yields two distinct access and refresh tokens from one code. Concurrent refresh token rotation produces a token the user's logout cannot revoke.
CVE-2024-30088 — Windows Kernel (CISA KEV, June 2024, CVSS 7.0)
A TOCTOU race in NtQueryInformationToken allowed local privilege escalation to SYSTEM. Added to CISA's Known Exploited Vulnerabilities catalog in June 2024 with confirmed active exploitation. This is the category's canonical kernel TOCTOU example.
CVE-2025-22224 — VMware ESXi/Workstation (CISA KEV, March 2025, CVSS 9.3) A TOCTOU heap overflow in the VMCI (Virtual Machine Communication Interface) subsystem enabled guest VM escape to the hypervisor host. Exploited in active campaigns targeting cloud and VDI environments before the March 2025 patch. CVSS 9.3 reflects complete hypervisor compromise from within a tenant VM.
CVE-2024-50379 + CVE-2024-56337 — Apache Tomcat (CVSS 9.8)
On case-insensitive file systems (Windows, macOS), Tomcat's JSP compilation checks filename case to determine if a file is a JSP. A race between uploading FILE.JSP and file.txt simultaneously — same target on the case-insensitive FS — allows the weaponized JSP to be compiled and executed via GET /file.jsp. CVE-2024-56337 was an incomplete fix bypassable with different case permutations. Fixed in Tomcat 11.0.2 / 10.1.34 / 9.0.98.
CVE-2024-58248 — nopCommerce Gift Card Double-Spend (CVSS 7.5)
Concurrent gift card redemption requests in nopCommerce versions before 4.80.0 passed the balance check before any UPDATE committed. A single $100 gift card could be redeemed N times in parallel, generating N × $100 in store credit. Outpost24 confirmed exploitability with a 20-request single-packet burst.
CVE-2024-45300 — alf.io Promo Code Race (CVSS 7.5)
The alf.io event ticketing platform (versions ≤ 2.0-M4-2402) allowed concurrent promo code applications to bypass usage limits. Multiple attendees could simultaneously apply a single-use promo code, bypassing the maximum-uses constraint. Fixed via atomic UPDATE WHERE count < max pattern.
HackerOne #55140 — OAuth 2 Race (Internet Bug Bounty, $2,500) Concurrent authorization code exchange requests yielded two distinct access token and refresh token pairs from a single code. The attacker retained a valid token pair even after the victim performed a full logout and revoked all sessions, because the attacker's parallel exchange produced a token family that was never associated with the victim's revocation action.
InnoGames Email Activation Race ($2,000) Concurrent email activation requests on a gaming platform incremented a virtual currency counter N times instead of once per activation. A single activation email, replayed in 20 concurrent requests, granted 20× the welcome bonus diamonds. HackerOne community rating: 137 upvotes.
CVE-2025-38352 — Linux Kernel POSIX Timer TOCTOU (CISA KEV, September 2025, CVSS 7.4) A TOCTOU in POSIX CPU timer handling allowed local privilege escalation. Added to the CISA KEV catalog in September 2025, this is the third kernel-level race condition in the catalog within 15 months — confirming that TOCTOU races in OS kernels remain an actively exploited attack class.
/redeem, /apply, /withdraw, /transfer, /confirm, /vote, /refresh, /oauth/token.200 OK where only one is expected is a preliminary signal.Turbo Intruder with Engine.BURP2 remains the standard for PoC confirmation:
# Turbo Intruder — single-packet gate attack
def queueRequests(target, wordlists):
engine = RequestEngine(
endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2 # HTTP/2 single-packet
)
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)Key signals for automated detection:
BreachVex detects race conditions through multiple complementary techniques: structural pattern matching on transactional URL paths, concurrent HTTP/2 bursts with response-differential analysis, and read-race-read state proof before confirming any finding. Financial endpoints (withdraw, transfer) always require dual-judge validation before marking Critical.
The most reliable defense eliminates the check-and-use gap at the database level:
-- VULNERABLE: separate SELECT then UPDATE — race window between them
SELECT used FROM coupons WHERE code = 'SAVE25';
-- ...application logic...
UPDATE coupons SET used = TRUE WHERE code = 'SAVE25';
-- FIXED: atomic conditional UPDATE — check and act in one statement
UPDATE coupons
SET used = TRUE, redeemed_by = $1, redeemed_at = NOW()
WHERE code = $2 AND used = FALSE
RETURNING id, amount;
-- 0 rows returned = already used — no separate SELECT neededFor balance-draining scenarios, SELECT FOR UPDATE within a transaction acquires a row-level write lock:
BEGIN;
SELECT balance FROM accounts WHERE id = $1 FOR UPDATE;
-- Concurrent transactions BLOCK here until this transaction commits
UPDATE accounts SET balance = balance - $2 WHERE id = $1 AND balance >= $2;
COMMIT;For cross-service flows where database transactions cannot span service boundaries, Redis SETNX (SET if Not eXists) provides atomic deduplication:
import redis.asyncio as redis
import uuid
async def redeem_coupon_safe(code: str, user_id: int, r: redis.Redis):
lock_key = f"coupon_lock:{code}"
lock_id = str(uuid.uuid4())
# Atomic: SET key only if absent (NX) with 10s expiry
acquired = await r.set(lock_key, lock_id, nx=True, ex=10)
if not acquired:
raise ValueError("Concurrent redemption in progress — retry")
try:
coupon = await db.get_coupon(code)
if coupon.used:
raise ValueError("Coupon already redeemed")
await db.mark_coupon_used(code, user_id)
finally:
# Lua: only delete if we own the lock — prevents cross-release
await r.eval(
"if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end",
1, lock_key, lock_id
)For API endpoints, enforce an Idempotency-Key header — a client-supplied UUID the server deduplicates:
@router.post("/transfer")
async def transfer(body: TransferRequest, idempotency_key: str = Header(...)):
cached_key = f"idem:{idempotency_key}"
if existing := await redis.get(cached_key):
return existing # Return cached result for duplicate request
result = await do_transfer(body)
await redis.setex(cached_key, 86400, result.model_dump_json())
return resultRaise the PostgreSQL isolation level to SERIALIZABLE for financial operations:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Full conflict detection — raises SerializationFailure on concurrent commit
-- Application retries transparently. No manual locks needed.Avoid the read-modify-write ORM anti-pattern:
# VULNERABLE — read into memory, modify, save back (race window)
account = db.query(Account).filter_by(id=account_id).first()
account.balance -= amount
db.commit()
# FIXED — atomic expression evaluated in DB, no round-trip
from sqlalchemy import update
db.execute(
update(Account)
.where(Account.id == account_id, Account.balance >= amount)
.values(balance=Account.balance - amount)
)
db.commit()The same pattern applies across ORMs:
| ORM Pattern | Vulnerable | Fixed |
|---|---|---|
| Rails | User.find(id).update(balance: b - 100) | User.where(id: id).update_all("balance = balance - 100") |
| Mongoose | findOne({id}).update(...) | findOneAndUpdate({id}, {$inc: {balance: -100}}) |
| Django | obj = Model.objects.get(); obj.field += 1; obj.save() | Model.objects.filter(...).update(field=F('field') + 1) |
| Hibernate | entity.setX(); session.merge(entity) without @Version | Add @Version field for optimistic locking |
Limiting HTTP/2 concurrent streams (http2_max_concurrent_streams 10 in nginx, SETTINGS_MAX_CONCURRENT_STREAMS=10 in server config) reduces the single-packet attack window but does not eliminate it. An attacker can still achieve 10 concurrent requests — sufficient for most limit-overrun attacks. Combine stream limits with atomic business logic defenses.
A race condition occurs when the outcome of a security-critical operation depends on the timing or ordering of concurrent events. In web applications, the classic pattern is TOCTOU (Time-of-Check to Time-of-Use): the application reads a value to make a decision, then acts on that decision — but an attacker fires multiple concurrent requests to execute the action before the first request's state change has been committed. The result is a window where a security check passes multiple times for a single-use resource.
The single-packet attack (PortSwigger Research, Black Hat USA 2023) sends 20–30 HTTP/2 requests in a single TCP packet by withholding the final byte of each HTTP/2 DATA frame and releasing all of them simultaneously. The OS coalesces the final bytes via Nagle's algorithm, delivering all requests to the server within a sub-millisecond window. This eliminates intercontinental network jitter (15–30ms), making remote race conditions as reliable as local ones. Turbo Intruder's Engine.BURP2 and Burp Repeater's 'Send Group in parallel' both implement this technique.
First sequence sync (GMO Flatt Security, CODE BLUE 2024) extends the single-packet attack beyond its HTTP/2 Window Size limit of 65,535 bytes. By exploiting IP fragmentation and TCP sequence number reordering, it achieves approximately 10,000 concurrent requests in 166ms. The technique delays the first IP fragment containing TCP sequence number 0, forcing the server to buffer all subsequent fragments before processing any request in the window. This enables races that require thousands of concurrent participants, such as airdrop multi-claim or DAO governance manipulation.
TOCTOU (Time-of-Check Time-of-Use, CWE-367) is the most common race condition pattern: an application checks a precondition (time-of-check), then acts on it (time-of-use) in a non-atomic sequence. An attacker exploits the gap between check and use. Web examples include: balance check followed by concurrent withdraw (balance drained); coupon-used check followed by concurrent redeem (coupon applied N times); permission check followed by concurrent action (access granted before revocation propagates).
JIT (Just-In-Time) inconsistency, identified by James Kettle in 2023, describes a partial construction window where an object exists in the database but is not yet fully initialized. Example: in Rails+Devise, a user row is inserted with confirmation_token=NULL before a background job populates the token. An attacker races a confirmation request with an empty token during that window, passing the token check. A default-permission window exists when a new user account starts with admin privileges before the privilege-downgrade step completes.
1. Identify a stateful endpoint with a guard condition (coupon apply, gift card redeem, one-time password check, withdrawal). 2. In Burp Repeater, create a group of 20 identical requests. 3. Select 'Send group in parallel (single-packet attack)' — this uses the built-in HTTP/2 last-byte sync. 4. Look for multiple 200 OK responses where only one is expected. 5. Confirm by reading server state post-race: if coupons_used incremented by N instead of 1, the race is real. Connection warming (1–3 harmless requests before the burst) improves reliability for cold backends.
Turbo Intruder (Burp extension) with Engine.BURP2 is the standard PoC tool for HTTP/2 single-packet attacks. Burp Repeater's 'Send group in parallel' (since Burp 2023.10) provides a simpler UX for manual testing. h2spacex (Python + Scapy) gives low-level HTTP/2 frame control for precise last-byte timing. httpx with http2=True and asyncio.gather handles simpler concurrent tests in Python. Race-the-Web is a legacy Go tool for HTTP/1.1 last-byte synchronization.
An idempotency key is a client-supplied UUID that the server uses to deduplicate requests. On first receipt, the server processes the request, stores the result keyed by the idempotency key (e.g., Redis SETNX with 24h TTL), and returns it. On subsequent requests with the same key, the server returns the cached result without re-processing. This prevents duplicate charges, double-redemptions, and duplicate account creation. Stripe, PayPal, and Braintree mandate idempotency keys for all payment mutations.
SELECT FOR UPDATE acquires a row-level write lock in the database transaction. Any concurrent transaction attempting to SELECT or UPDATE the same row blocks until the first transaction commits or rolls back. This makes the check-and-use sequence atomic at the database level: the second request cannot read the pre-update state because it is blocked by the first transaction's lock. PostgreSQL, MySQL, and Oracle all support SELECT FOR UPDATE.
Three are in the CISA KEV catalog (actively exploited): CVE-2024-30088 (Windows Kernel TOCTOU, CVSS 7.0, June 2024), CVE-2025-22224 (VMware ESXi TOCTOU VM escape, CVSS 9.3, March 2025), CVE-2025-38352 (Linux kernel POSIX timer TOCTOU, CVSS 7.4, September 2025). High-severity web races: CVE-2024-50379 (Apache Tomcat JSP TOCTOU RCE, CVSS 9.8), CVE-2024-58248 (nopCommerce gift card double-spend, CVSS 7.5), CVE-2024-45300 (alf.io promo code bypass, CVSS 7.5), CVE-2025-32421 (Next.js cache poisoning race).
Yes. The classical balance-drain attack sends N concurrent withdrawal requests. Each reads the balance before any UPDATE commits, so all pass the balance check. N withdrawals are processed but only one is debited. The Doyensec 'Database Race Conditions in AppSec' (2024) survey found this pattern in multiple fintech platforms using Hibernate with default READ COMMITTED isolation. Fix: PostgreSQL SELECT FOR UPDATE or SERIALIZABLE isolation level.
Rate limiters typically increment a per-user counter after each request is processed. With HTTP/2 single-packet attack, all N requests arrive before any counter increment completes, so they all read count=0 and pass the rate check. This is confirmed by Cloudflare and AWS WAF token-bucket behavior in 2024 bug bounty reports. The fix requires atomic deduplication at the business logic layer: SELECT FOR UPDATE or Redis SETNX, not just gateway rate limiting.
Connection warming sends 1–3 innocuous requests (GET /) on the same HTTP/2 connection before the race burst. This completes the TLS handshake, HTTP/2 SETTINGS frame exchange, and warms load-balancer routing tables. Without warming, cold-start variance (50–500ms for Lambda or Cloud Run cold starts) acts as accidental race protection by serializing requests. Warming ensures the connection is in steady-state for the attack window.
PHP native file-based sessions serialize requests per session via flock() on the session file. A second request with the same PHPSESSID blocks until the first completes — eliminating per-session races. However, this protection does not apply to JWT-based PHP applications, applications using Redis or Memcached session backends without explicit locking, or multi-endpoint races across different sessions.
A multi-endpoint race exploits a state transition that spans two or more endpoints. Example: an email confirmation flow where /confirm marks email as verified before /login checks the verification status — racing these two requests logs in with an unverified email. The state machine is insecure because neither endpoint individually is atomic with respect to the other. Kettle documented multi-endpoint races in 2023 as one of three new race categories enabled by the single-packet attack.
OWASP Top 10 2021 A04:2021 (Insecure Design) covers race conditions under improper state machine design. OWASP ASVS V11.1.6 explicitly requires TOCTOU testing in the Business Logic verification section. OWASP WSTG-BUSL-04 (Process Timing) and WSTG-BUSL-05 (Number of Times a Function Can Be Used) provide test case specifications. CWE-362 is the base weakness; CWE-367 (TOCTOU) is the most common subtype.