Race Condition (TOCTOU) - Timing-basierte Sicherheitsschwachstelle
Race conditions (CWE-362) occur when a system's security depends on two or more operations being executed in a specific order, but parallel execution violates that order. TOCTOU (Time-Of-Check Time-Of-Use) is the most common form: checking and using a resource occur at different times. Security implications: double spending in financial applications, privilege escalation via temporary files, discount abuse, account takeover. Protection: atomic database operations, mutexes, optimistic locking.
Race Conditions are among the harder-to-find but often high-impact vulnerabilities. A classic example: An online banking application checks the account balance (€50) and then initiates a transfer. What if an attacker sends 100 transfer requests simultaneously? Each check shows €50, and each transfer is executed before the account balance is updated. Result: €5,000 debited instead of €50.
TOCTOU - Time Of Check / Time Of Use
Basic Principle
Normal (insecure) flow:
t=0: CHECK: Account balance = €50, transfer €50 → OKt=1: ACTION: Debit €50, account balance = €0
Race Condition (parallel requests):
- Request A,
t=0: CHECK: Account balance = €50 → OK - Request B,
t=1: CHECK: Account balance = €50 → OK (not yet updated!) - Request A,
t=2: ACTION: Debit €50, account balance = €0 - Request B,
t=3: ACTION: Debit €50, account balance = -€50 (!)
> The time window between CHECK and USE = "Race Window". The larger the Race Window, the easier it is to exploit. Database calls, network requests, and I/O increase the window.
TOCTOU Categories
1. File System TOCTOU:
// Check if file exists:
if (!file_exists($filename)) {
file_put_contents($filename, $data); // ← Between check and use: symlink!
}
// Attacker creates a symlink between file_exists() and file_put_contents()
// Writes data to /etc/passwd or similar (if the process has root privileges)
2. Web Application TOCTOU (Business Logic):
- Check if user has sufficient balance
- Deduct balance
- Race Window: send parallel HTTP requests
3. Operating System Level:
- Processes with SUID bit: check file existence, then open
- Between check and open: swap file
- Privileged process now opens a different file than expected
Impact on Web Applications
1. Redeeming Gift Cards / Coupon Codes Multiple Times
Normal Process:
- Check: Is the coupon valid? (Status = unused)
- Apply discount
- Mark coupon as used
Race Condition:
- Attacker sends 50 parallel requests with the same code
- All check: Status = unused → all OK
- All apply discount (before one sets "used")
- Coupon redeemed 50 times!
Proof: Burp Suite Turbo Intruder
POST /apply-coupon { "code": "SAVE50" }
→ 50 parallel requests → check if credited multiple times
2. Double Spending
Crypto Wallet or E-Commerce:
- Check: Wallet balance ≥ 100
- Send transaction: -100
- Update wallet: -100
Race: send parallel transactions; both check 100 ≥ 100 → true. Both transactions are executed → effectively, 200 is spent from a 100 balance.
3. Limit Bypass (Rate Limiting via DB)
"User may create a maximum of 1 account":
- Check: Does the user already have an account? (COUNT(*) = 0)
- Create account
- Result: Account exists
Race: 100 parallel registration requests → all check: 0 accounts → true → all create accounts → User has 100 accounts!
4. TOCTOU during privilege change
Admin Panel: "Deactivate User Account":
- Check: Is the account active? (active = true)
- Invalidate sessions
- Set active = false
Race: User sends request at the same moment → Check: active = true → OK → Deactivation in progress → User request occurs between check and deactivation → still authorized!
Detection and Testing
Burp Suite Turbo Intruder
Specifically optimized for race condition testing: HTTP pipelining + simultaneous TCP connections, very precise timing (millisecond accuracy).
Basic Race Condition Test:
- Identify HTTP request (e.g., POST /apply-coupon)
- In Burp: Send to Turbo Intruder
- Payload: 50 requests with race=true
- Send simultaneously (not sequentially!)
- Analyze responses: more than 1 success?
Turbo Intruder Configuration:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=50,
pipeline=False) # Separate connections!
for i in range(50):
engine.queue(target.req) # Queue all immediately
def handleResponse(req, interesting):
if '200' in req.status:
table.add(req) # Mark successful responses
HTTP/2 Single-Packet Attack:
- All requests in a single TCP packet
- Maximum synchronization → highest race window hit rate
- Supported starting with Burp Suite 2022.9
Testing Steps for Business Logic
- Gift Cards: Redeem the same code 20 times simultaneously
- Bank Transfers: Transfer the same amount 10 times simultaneously
- Registration: Create the same username 10 times simultaneously
- Password Reset: Redeem the same token 5 times simultaneously
- Discount: Apply the same code 20 times simultaneously
Indicators of Race Conditions
- Multiple "success" responses to identical parallel requests
- Different response times in parallel tests
- Inconsistent database states after tests
Mitigation Measures
1. Database Level - Atomic Operations
-- Unsafe:
SELECT balance FROM accounts WHERE id = 123; -- balance = 100
UPDATE accounts SET balance = balance - 50 WHERE id = 123;
-- Safe (atomic operation):
UPDATE accounts
SET balance = balance - 50
WHERE id = 123 AND balance >= 50;
-- Affected rows: 0 → Retry or error; Affected rows: 1 → Success!
-- SELECT FOR UPDATE (Pessimistic Locking):
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 123 FOR UPDATE;
-- Row is now locked! No other thread can modify it
UPDATE accounts SET balance = balance - 50 WHERE id = 123;
COMMIT;
2. Database Level - Unique Constraints
-- Prevents duplicate issuance of coupon codes:
CREATE TABLE coupon_redemptions (
coupon_code VARCHAR(50) NOT NULL,
user_id INT NOT NULL,
UNIQUE (coupon_code, user_id) -- DB prevents duplicates!
);
-- INSERT will fail if already exists → Race condition prevented!
3. Application Level - Mutex/Semaphore
import threading
lock = threading.Lock()
def transfer_money(from_account, to_account, amount):
with lock: # ← Exclusive access
balance = get_balance(from_account)
if balance >= amount:
set_balance(from_account, balance - amount)
set_balance(to_account, get_balance(to_account) + amount)
# Redis-based distributed locking (for scaled apps):
import redis
r = redis.Redis()
lock_key = f"transfer_lock_{account_id}"
if r.set(lock_key, "1", nx=True, ex=5): # NX = only if it doesn't exist
try:
perform_transfer()
finally:
r.delete(lock_key)
else:
raise Exception("Transfer already in progress, retry")
4. Idempotency Keys (for APIs)
Every critical action is assigned a unique key:
POST /api/transfer
Idempotency-Key: uuid-generated-by-client
Server-side:
- Check: has this key already been processed?
- Yes: return cached response (do not re-execute!)
- No: process and store the key with the result
Prevents duplicate execution even during network retries. Standard in payment APIs (Stripe and PayPal use this pattern).
5. TOCTOU in the File System
// Use O_EXCL + O_CREAT (atomic creation):
fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd == -1 && errno == EEXIST) {
// File already exists → error
}
// O_EXCL: error if file already exists
// Atomic: no race condition between check and creation!