Race conditions (CWE-362) quick reference: TOCTOU, limit-overrun, multi-endpoint, partial construction — attack surface mapping and decision tree.
TL;DR
Web race conditions divide into eight categories defined by the attack surface, the state machine flaw, and the exploitation technique. Understanding the category determines both the detection approach and the correct fix.
TOCTOU (Time-of-Check to Time-of-Use, CWE-367) is the foundational pattern: an application reads state to make a decision (check) then acts on that decision (use) without atomicity. The attacker wins the window between check and use. This appears in file systems (CVE-2024-50379 Tomcat JSP compilation), databases (balance check then withdrawal), and kernels (CVE-2024-30088, CVE-2025-22224 — both CISA KEV).
Limit-overrun targets any enforced counter: usage limits on coupons, gift cards, API calls, votes, faucet claims. The counter check and increment are not atomic — concurrent requests all read the pre-increment value, all pass the check, and all increment. Covers CVE-2024-45300 (alf.io promo codes) and CVE-2024-58248 (nopCommerce gift cards).
Multi-endpoint race exploits state transitions that span two separate endpoints. The state machine requires completing endpoint A before accessing endpoint B, but the transition is not atomic — racing a request to B between A's write and A's commit bypasses the requirement. Classic example: racing /login between the /signup write and the email verification /confirm commit.
JIT inconsistency (Partial Construction, Kettle 2023) exploits a window where an object exists in the database but is not yet fully initialized. Rails+Devise creates a user with confirmation_token=NULL before a background job populates it — an attacker races a confirmation with an empty token during the NULL window. Similarly, a default-permission window exists between account creation with admin privileges and the privilege-downgrade step.
OAuth/JWT races target authorization code single-use enforcement (HackerOne #55140 — two token pairs from one code) and refresh token rotation (concurrent rotation producing a spare valid token that survives user logout).
HTTP/2-specific races (the enabling layer): the single-packet attack and first sequence sync are not vulnerability types but exploitation techniques that enable all the above categories at scale. HTTP/2 last-byte synchronization delivers all concurrent requests within sub-millisecond windows, making previously-impractical timing attacks industrially reliable.
| Category | Surface | Tools | Fix |
|---|---|---|---|
| TOCTOU (DB) | Fintech, e-commerce, auth | Turbo Intruder, httpx | SELECT FOR UPDATE, atomic UPDATE |
| TOCTOU (filesystem) | File upload handlers, build systems | Concurrent upload PoC | Upload to non-webroot temp dir |
| TOCTOU (kernel) | OS, hypervisors | Local exploit | Vendor patch (CISA KEV) |
| Limit-overrun | Coupon, vote, faucet, quota | Turbo Intruder | INSERT ON CONFLICT DO NOTHING |
| Multi-endpoint | Auth flows, checkout | Burp Repeater group | State machine audit |
| JIT inconsistency | Rails+Devise, user creation | HTTP/2 concurrent | Synchronous initialization |
| OAuth/JWT | Auth providers | asyncio.gather | Atomic code invalidation |
| CDN cache race | Next.js ISR, edge functions | Concurrent GET | Revalidation locking |
The single-packet attack and first sequence sync are complementary techniques, chosen based on the number of concurrent requests needed:
| Technique | Max Concurrent | Window | Use When |
|---|---|---|---|
| HTTP/1.1 last-byte sync | 3–5 | 5–20ms | HTTP/1.1 only targets |
| HTTP/2 single-packet (Kettle 2023) | 20–30 | <1ms | Standard races, HTTP/2 required |
| First sequence sync (Flatt 2024) | ~10,000 | 166ms | High-concurrency races (faucet, airdrop) |
HTTP/2 single-packet: withhold the final byte of each HTTP/2 DATA frame across all N requests, then release simultaneously. The OS Nagle algorithm coalesces all final bytes into one TCP packet, delivering all requests to the server within under 1ms. Implemented in Turbo Intruder (Engine.BURP2), Burp Repeater "Send group in parallel", and h2spacex.
First sequence sync: exploits IP fragmentation to delay the first IP fragment (TCP sequence number 0) of an HTTP/2 connection while all subsequent fragments proceed. The server waits for the delayed fragment before processing any request in the connection window, achieving synchronization of up to 10,000 concurrent requests. Requires raw packet access via Scapy (h2spacex extension). Primarily relevant for airdrop/faucet races requiring massive concurrency.
Race condition detection combines several complementary techniques:
Structural identification: identify transactional URL patterns — /redeem, /transfer, /withdraw, /vote, /claim, /oauth/token, /reset, /confirm. These are candidates regardless of response behavior.
Burst with response differential:
async def burst_probe(url: str, headers: dict, body: dict, n: int = 20):
"""Send N concurrent requests — analyze response distribution."""
async with httpx.AsyncClient(http2=True, verify=False) as client:
await client.get(url.rsplit("/", 1)[0] + "/", headers=headers) # Warm
tasks = [client.post(url, headers=headers, json=body) for _ in range(n)]
responses = await asyncio.gather(*tasks, return_exceptions=True)
statuses = [r.status_code for r in responses if not isinstance(r, Exception)]
successes = statuses.count(200)
return {"n": n, "successes": successes, "race_signal": successes > 1}Read-race-read state proof: GET authoritative state before burst, execute burst, GET state after. If the counter incremented by N instead of 1, or N distinct resource IDs were created, the race is confirmed. This eliminates false positives from cached responses, server-side retry logic, and idempotency that returns cached 200s.
Does the race target a guard condition on an EXISTING, FULLY-INITIALIZED resource?
├── YES → TOCTOU (CWE-367)
│ └── Is the guard on a file path? → Filesystem TOCTOU
│ └── Is the guard on a DB column? → Database TOCTOU
│
├── NO → Does it target a window during OBJECT CREATION/INITIALIZATION?
│ └── YES → JIT Inconsistency (Kettle 2023)
│ └── NULL token window? (Devise pattern)
│ └── Default-permission window? (admin before downgrade)
│
└── NO → Does it exploit TWO SEPARATE ENDPOINTS?
└── YES → Multi-endpoint race (CWE-841)
└── Auth bypass pattern? (signup + confirm + login)
└── Permission race? (grant + action before revoke)| CVE | Category | CVSS | CISA KEV | Impact |
|---|---|---|---|---|
| CVE-2024-30088 | Kernel TOCTOU | 7.0 | YES | Windows SYSTEM LPE |
| CVE-2025-22224 | Hypervisor TOCTOU | 9.3 | YES | VMware ESXi VM escape |
| CVE-2025-38352 | Kernel TOCTOU | 7.4 | YES | Linux kernel LPE |
| CVE-2024-50379 | Filesystem TOCTOU | 9.8 | NO | Apache Tomcat RCE |
| CVE-2024-58248 | Limit-overrun | 7.5 | NO | nopCommerce gift card drain |
| CVE-2024-45300 | Limit-overrun | 7.5 | NO | alf.io promo code bypass |
| CVE-2025-32421 | CDN cache race | 5.4 | NO | Next.js cache poisoning |
| HackerOne #55140 | OAuth race | N/A | NO | Token family persistence ($2.5K) |
Three confirmed CISA KEV entries in 15 months (June 2024–September 2025) confirm that race conditions — especially TOCTOU at the kernel and hypervisor level — are not theoretical. They are exploited within weeks of disclosure and targeted by advanced threat actors.
TOCTOU (database): atomic conditional UPDATE (UPDATE WHERE condition RETURNING id) or SELECT FOR UPDATE within a transaction. Never separate SELECT then UPDATE.
Limit-overrun: INSERT ON CONFLICT DO NOTHING + conditional counter increment only on successful insert. Or atomic UPDATE WHERE count < max RETURNING id.
Duplicate transaction: Idempotency-Key header (Redis SETNX, 24h TTL) + database UNIQUE constraint on operation ID as fallback.
File upload: upload to non-web-accessible temp directory, validate completely, then move atomically to webroot. Never write to webroot first.
OAuth/JWT: atomic code invalidation — UPDATE codes SET used=true WHERE id=$1 AND used=false RETURNING access_token. If 0 rows, code was already consumed.
JIT inconsistency: synchronous initialization — complete all setup steps before the object becomes externally visible. Never create a row in the database that is incompletely initialized and accessible to other requests.
James Kettle's 2023 taxonomy identifies eight categories: (1) Classical TOCTOU — check-and-use gap on file system or database; (2) Limit-overrun — coupon, gift card, vote, faucet bypass via non-atomic counter; (3) Multi-endpoint race — state transition spanning two endpoints; (4) Single-endpoint collision — write-write race on shared session state; (5) JIT inconsistency — partial construction window where object is created but not fully initialized; (6) OAuth/JWT race — code double-redemption, refresh token rotation; (7) HTTP/2-specific — single-packet attack, first sequence sync; (8) Infrastructure-level — CDN cache revalidation, distributed lock race.
TOCTOU (Time-of-Check Time-of-Use) races an explicit check against an explicit action on an existing resource: balance >= amount check vs. withdrawal action. JIT (Just-In-Time) inconsistency races against an object's initialization window — the object exists in the database but its state is incomplete. Example: Rails+Devise creates a user row with confirmation_token=NULL before a background job populates the token. The attacker exploits the NULL window, not a guard condition on an already-complete object.
Single-endpoint races target one endpoint that has a guard condition and state change on the same resource (coupon redeem, balance withdraw). Multi-endpoint races exploit a state machine spanning two endpoints where the transition is not atomic: /signup (creates account with email_verified=false) + /confirm-email (sets email_verified=true) — racing /login between these two steps. Test multi-endpoint races when the application's security model depends on completing a workflow in sequence.
Rails + Devise (partial construction / NULL token window, well-documented by Kettle). Spring Boot + Hibernate (default READ COMMITTED isolation — SELECT then UPDATE pattern common). Express + Mongoose (findOne + update instead of atomic findOneAndUpdate). Django without F() expressions (obj.field += 1; obj.save() pattern). PHP applications using JWT (native session locking doesn't apply). Next.js 14.x-15.1.x (CVE-2025-32421 ISR cache revalidation race).
James Kettle's 'Smashing the State Machine' (Black Hat USA 2023) demonstrated the single-packet attack: 20–30 HTTP/2 requests delivered in one TCP packet via last-byte synchronization, eliminating network jitter and making remote races as reliable as local ones. This upgraded the exploitability of all web race conditions from 'impractical at distance' to 'industrially exploitable'. GMO Flatt Security's CODE BLUE 2024 'Beyond the Limit' extended this with first sequence sync — up to 10,000 concurrent requests in 166ms via IP fragmentation — enabling high-concurrency races previously impossible over the internet.
Look for endpoints that (1) enforce a single-use or limit constraint (redeem, claim, apply, withdraw, vote), (2) perform a state transition (confirm, activate, verify, reset), (3) create unique resources with uniqueness constraints (register, signup, issue-token), or (4) involve financial operations (transfer, payout, purchase). Any endpoint that reads state to make a decision and then mutates that state in a non-atomic sequence is a candidate. The structural signal — not the response — is what matters: a 409 Conflict on sequential requests with a 200 on concurrent requests is the classic confirmation.
Read-race-read (RRR) is the confirmation technique that eliminates false positives from cached 200 responses or server-side retry logic. Step 1: GET authoritative state (balance, counter, token status) and record the value. Step 2: execute the N-concurrent burst. Step 3: GET authoritative state again. If the state changed by more than one unit — balance dropped by N×amount instead of 1×amount, counter incremented by N instead of 1, N distinct resource IDs were created — the race is confirmed regardless of response codes. RRR is required for any race finding above Informational severity.
SERIALIZABLE isolation prevents all race conditions at the database level by detecting write-write and read-write conflicts between concurrent transactions and aborting one. READ COMMITTED (the PostgreSQL default) does not prevent TOCTOU — each statement sees a consistent snapshot but two statements in the same transaction can see different states if a concurrent write commits between them. READ REPEATABLE prevents non-repeatable reads within a transaction but still allows phantom rows. In practice: use SERIALIZABLE for critical financial operations, or use the more targeted SELECT FOR UPDATE / atomic conditional UPDATE patterns which achieve safety without the full serialization overhead.
Standard limit-overrun races a counter increment — the counter is the guard. OAuth/JWT races target single-use token invalidation: the authorization code, reset token, or refresh token must be marked consumed exactly once. The race exploits the window between the validity check (SELECT valid=true) and the invalidation (UPDATE used=true or DELETE). The consequence is qualitatively different: limit-overrun produces extra resource consumption (more credits, more votes), while OAuth races produce persistent unauthorized access — an attacker retains a valid token family after the victim's session is revoked.
Gateway rate limiting and WAF rules do not prevent application-level race conditions. Rate limiters operate on request counts per time window, not on the atomicity of the application's state transitions. The single-packet attack delivers all N requests within under 1ms — faster than any token-bucket counter can increment between request 1 and request 2. WAF rules cannot inspect whether a SELECT and UPDATE are atomic. Application-level fixes (atomic SQL, idempotency keys, SELECT FOR UPDATE) are the only effective countermeasures. WAF rules can reduce abuse volume but cannot close the race window.