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.
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.
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.
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.
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.
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 class | Typical entry point | Confirming artifact | Severity anchor |
|---|---|---|---|
| Balance overdraft | Debit or withdrawal endpoint | Multiple 200s on a single-use balance; balance driven below zero | High - financial integrity broken |
| Coupon / token replay | Redemption or apply endpoint | Multiple 200s on a one-time code | Medium to High - depends on coupon value |
| Rate-limit bypass | Login, OTP, brute-force-protected path | Requests that should be blocked succeed inside the count window | High - enables brute force past the intended cap |
| File TOCTOU | Upload handler that checks MIME before moving | Second upload lands a different type at the verified path | Critical if the file is later executed or parsed |
| Privilege window | Role assignment or elevation flow | Elevated access observable during the transition state | Critical - 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.
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.
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.