AppSec · Jul 4, 2026 · 7 min read

Race conditions and TOCTOU: confirming the window is real

A check-then-act pattern that opens a window for concurrent abuse is reachable - a confirmed race means two requests collide inside that window with a measurable outcome.

Race conditions surface when two requests arrive inside the narrow window between a check and the act that follows it. The code is correct in single-threaded execution; the gap only opens when a second request arrives before the first one completes the action that makes the check meaningful. Most scanners never discover them because they issue one request at a time - the window closes before the probe arrives.

01The check-then-act pattern and where it hides

A race condition requires three things: a shared state, a check on that state, and an action that depends on the check. Common patterns are a coupon that reads is_used before marking it used, a wallet that reads balance before writing a debit, a rate limiter that counts requests before incrementing, and a file upload that verifies a MIME type before moving the file to a permanent path. Each of these is safe in serial execution and unsafe the moment two requests share the window between the read and the write.

Thread A Thread B ──► read balance = $50 ──► read balance = $50 ──► balance >= $50? YES ──► balance >= $50? YES ──► debit $50 / write $0 ──► debit $50 / write $0 (stale read) result: $50 debited twice from a $50 account both reads completed before either write committed
Two threads pass the same balance check on the same pre-write snapshot. The window can be microseconds wide and is invisible in code review.

The endpoints that carry this pattern tend to be the highest-value ones: payments, redemptions, inventory decrements, role assignments, password resets. The check exists because the developer knew the constraint; the race exists because they assumed serial execution.

02Confirming the window with concurrent probes

Confirming a race requires sending two or more requests that arrive inside the TOCTOU window simultaneously - and then observing a response pattern that only makes sense if both passed the same check on the same state snapshot. The probe must be non-destructive: a benign, reversible action with a detectable outcome, not a real fund transfer or a real privilege escalation. A threading barrier synchronises the release so all requests fire at the same instant rather than serially.

race-probe.pypython
import threading, requests

# nonce coupon - no real monetary value attached
TARGET  = "https://app.example/api/redeem"
PAYLOAD = {"coupon": "NONCE-62615533", "action": "apply"}

results = []
def send(session):
    r = session.post(TARGET, json=PAYLOAD)
    results.append(r.status_code)

# barrier ensures all threads release at the same moment
N = 5
sessions = [requests.Session() for _ in range(N)]
barrier  = threading.Barrier(N)
threads  = []
for s in sessions:
    t = threading.Thread(
        target=lambda s=s: (barrier.wait(), send(s))
    )
    threads.append(t); t.start()
for t in threads: t.join()

# >1 success means two requests passed the same is_used check
print(f"results: {results}")

The confirming artifact is the response set: one 200 OK is expected, two or more mean the check was bypassed. The probe uses a nonce coupon that holds no real value - the observable outcome is the HTTP status, not a real financial consequence. Repeat the run three times to rule out network timing flukes; a race that fires on every run is a structural gap, not noise.

Proven - HighRace condition - coupon redemption window - /api/redeem
5x concurrent POST /api/redeem {"coupon":"NONCE-62615533"} via barrier ──► 3x 200 OK (coupon accepted three times in the same window) ──► 2x 409 Conflict (arrived after the first write committed) Reproduced across 4 independent runs; 2-4 acceptances each time.
Oracle: parallel response-code distribution. No real value transferred; nonce coupon with no account balance attached. Severity high - a single valid coupon is redeemable multiple times in an authenticated session.

03Race classes and their confirming artifacts

Races are not a single pattern. Each class has a different entry point, a different confirming signal, and a different real-world impact. Testing one class while ignoring the others leaves the rest of the surface unchecked.

Race classTypical entry pointConfirming artifactSeverity anchor
Balance overdraftDebit or withdrawal endpointMultiple 200s on a single-use balance; balance driven below zeroHigh - financial integrity broken
Coupon / token replayRedemption or apply endpointMultiple 200s on a one-time codeMedium to High - depends on coupon value
Rate-limit bypassLogin, OTP, brute-force-protected pathRequests that should be blocked succeed inside the count windowHigh - enables brute force past the intended cap
File TOCTOUUpload handler that checks MIME before movingSecond upload lands a different type at the verified pathCritical if the file is later executed or parsed
Privilege windowRole assignment or elevation flowElevated access observable during the transition stateCritical - temporary privilege readable or persistent

File TOCTOU and privilege-window races are the severity inflection points. An upload handler that verifies MIME type before moving the file to a serve path opens a race between the check and the move. Confirming it without deploying a real webshell uses the same probe discipline: a second upload of a benign file with a mismatched extension, timed to arrive after the check and before the move, and then observing whether the serve path reflects the second file content. If it does, the check is cosmetic - the file at the destination is not what the check approved.

04Severity and what actually fixes the window

Race conditions inherit their severity from the confirmed outcome, not from the mechanism. A confirmed coupon replay with no real balance is medium; a confirmed balance overdraft is high; a confirmed privilege escalation or file-type bypass is critical. The finding is rated at what was demonstrated in the probe - never extrapolated to a higher impact that was not observed - with the full response set attached as the evidence artifact.

0
destructive writes during probing
5
concurrent sessions per window probe
3
minimum repeats before confirming

Remediation is at the shared-state layer, not at the request layer. Database-level serializable transactions or optimistic locking - a check-and-set with a version token - close the window regardless of how many concurrent requests arrive. Atomic operations remove the gap entirely: a single UPDATE coupons SET used=1 WHERE id=? AND used=0 reads and writes in one step, leaving no window for a second reader to observe the pre-write state. Rate limiting and idempotency keys are mitigations, not fixes; they reduce the probability of a successful race without closing the underlying window. The fix is structural - remove the window, not the arrival rate.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →