Race Conditions
Race Conditions - Overview
1. What it is
App processes two+ requests concurrently, both touch the same data, no safeguard against it β a "collision"
Attacker sends carefully timed requests on purpose to trigger that collision
Closely related to business logic flaws - same root idea (app in a state the devs didn't plan for), just triggered by timing instead of input
Race window = the tiny gap where the collision is possible (often just milliseconds)
2. Limit overrun - the classic type
Exceeds a limit the business logic is supposed to enforce
Why it happens: check β apply β update, done as separate steps, not atomically
Check code hasn't been used
Apply discount
Update DB to mark code as used
Fire two requests before step 3 finishes on the first one β both pass the check β limit bypassed
Common variations:
Redeem a gift card / discount code multiple times
Rate a product multiple times
Withdraw/transfer more than account balance
Reuse one CAPTCHA solution
Bypass anti-brute-force rate limiting
Technical name for this subtype: TOCTOU (time-of-check to time-of-use)
3. How to actually fire the requests together
Sending "at the same time" isn't good enough - network jitter throws off ordering
HTTP/1 β last-byte synchronization (hold back final byte of each request, release together)
HTTP/2 β single-packet attack - jams 20-30 requests into one TCP packet, removes jitter almost entirely
Burp Repeater (2023.9+) does this automatically via "send group in parallel"
Turbo Intruder (BApp store) - needed for heavier attacks: retries, staggered timing, huge request volume
Set
engine=Engine.BURP2,concurrentConnections=1Queue requests to a named
gate, fire withengine.openGate()
4. Beyond limit overruns - hidden multi-step sequences
A single request can internally pass through multiple hidden "sub-states" before finishing
Example: login sets session as logged-in before checking if MFA is required β race a login request against a sensitive authenticated request β land in the logged-in sub-state before MFA gets enforced
These are app-specific - no universal payload, need the methodology below to find them
5. Methodology - Predict, Probe, Prove
Predict
Probe
Benchmark normal behavior first - send the request group in sequence (separate connections)
Then send the same group in parallel (single-packet / last-byte sync)
Compare - anything different counts as a clue: response change, different email content, visible behavior shift afterward
Prove
Strip down to the minimum requests that reproduce it
Confirm you can repeat it reliably
Treat it as a structural weakness, not a one-off - the same flaw often has more than one angle of impact
6. Multi-endpoint race conditions
Race windows across different endpoints, not just one
Classic pattern: pay for cart β add more items β force-browse to confirmation, all racing the payment-validate-then-confirm step
Getting the windows to line up is harder than single-endpoint:
Back-end connection delays - usually affects all parallel requests equally, so it's often harmless. Test by "warming" the connection first (send a throwaway GET, then the real group) - if that smooths out timing, ignore the delay
Endpoint-specific processing delays - genuinely different processing times per endpoint. Fix by:
Turbo Intruder client-side delay (breaks single-packet attack, unreliable on jittery targets), or
Deliberately trip a rate/resource limit with junk requests to force a server-side delay, keeping single-packet viable
7. Single-endpoint race conditions
Parallel requests to the same endpoint, different values, from the same session
Example: password reset stores
reset-userandreset-tokenin session. Fire two resets, same session, different usernames β session can end up with victim's user ID paired with the token sent to the attacker's inboxNeeds the right interleaving of operations - may take several attempts
Good targets: anything email-based - emails are usually sent from a background thread after the response returns, widening the effective race window
8. Session-locking - a thing that can hide bugs
Some frameworks (e.g. PHP's native session handler) process one request per session at a time, serializing everything
If every request looks like it's running sequentially, that's the tell - try each request with a different session token to break the artificial serialization
9. Partial construction race conditions
Multi-step object creation leaves a temporary "half-built" middle state
Example: user created in DB, API key set in a separate statement β brief window where API key is uninitialized (empty/null)
Exploit: send a request where your supplied value also evaluates to that same uninitialized value
Framework array syntax tricks that produce empty/null values:
PHP:
param[]=fooβ['foo'], bareparam[]β[]Rails:
param[key](no value) β key present, valuenil
Example attack during the window:
GET /api/user/info?user=victim&api-key[]=Same idea works on passwords too, but since passwords are hashed, you need input that hashes to the same "empty" digest - harder
10. Time-sensitive attacks (not strictly races, same toolkit)
Precise request timing can expose weak token generation even without a true collision
Example: reset token derived from a timestamp instead of a secure random string β time two reset requests (different users) to land in the same timestamp tick β same token issued to both
11. Prevention (good context for reports)
Make sensitive state changes atomic - single DB transaction instead of separate check/update steps
Don't mix data across storage layers (e.g. session data can't safeguard a database limit)
Use datastore constraints (uniqueness, etc.) as a backup layer
Update session variables as a batch, not individually - same care needed with ORMs, they hide the transaction boundary
Where it fits the architecture, consider pushing state client-side (e.g. JWT) instead of server-side sub-states - though that trades in a different set of risks
Last updated