Race Conditions
Race conditions occur when applications process multiple requests concurrently without proper synchronization. Attackers exploit the timing gap between security checks and subsequent actions, a vulnerability class known as Time-of-Check to Time-of-Use (TOCTOU). These bugs can bypass business logic, duplicate rewards, and corrupt data.
What Are Race Conditions?
A race condition exists when the behavior of software depends on the sequence or timing of uncontrollable events. In web applications, this typically happens when multiple threads or processes access shared resources without proper locking mechanisms.
TOCTOU (Time-of-Check to Time-of-Use) is the most common pattern. The application checks a condition (like 'has this coupon been used?'), then performs an action based on that check. If an attacker can change the state between the check and the use, the security assumption is violated.
Common Attack Scenarios
Coupon/Discount Abuse
An e-commerce site checks if a single-use coupon is valid, then applies the discount. By sending multiple simultaneous requests, all might pass the validity check before any marks the coupon as used.
Balance/Withdrawal Double-Spend
A banking app checks account balance, then processes withdrawal. Multiple simultaneous withdrawal requests might all see the original balance and succeed, overdrawing the account.
Limit Bypass
Applications that limit actions (votes per user, downloads per day, API calls per minute) can have their limits bypassed when multiple requests arrive simultaneously before the counter is updated.
How Race Conditions Work
Consider this vulnerable coupon redemption flow:
// Vulnerable coupon redemption
async function redeemCoupon(userId, couponCode) {
// Step 1: Check if coupon exists and is unused
const coupon = await db.query(
'SELECT * FROM coupons WHERE code = ? AND used = false',
[couponCode]
);
if (!coupon) {
return { error: 'Invalid or used coupon' };
}
// RACE WINDOW: Time gap between check and update
// Multiple requests can pass the check above
// Step 2: Apply discount to user's cart
await applyDiscount(userId, coupon.discount);
// Step 3: Mark coupon as used
await db.query(
'UPDATE coupons SET used = true WHERE code = ?',
[couponCode]
);
return { success: true };
}The Race Window
Between the SELECT query and the UPDATE query, there's a window where the coupon is verified but not yet marked as used. If two requests enter this window simultaneously:
- Request A checks coupon → unused (passes)
- Request B checks coupon → still unused (passes)
- Request A applies discount
- Request B applies discount
- Both requests mark coupon as used
Testing for Race Conditions
Using Burp Suite Turbo Intruder
# Turbo Intruder script for race condition testing
def queueRequests(target, wordlists):
engine = RequestEngine(
endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=100,
pipeline=False
)
# Queue 30 identical requests
for i in range(30):
engine.queue(target.req, gate='race1')
# Release all requests simultaneously
engine.openGate('race1')
def handleResponse(req, interesting):
table.add(req)Using Python with Threading
import requests
import threading
import time
url = 'https://target.com/api/redeem'
headers = {'Authorization': 'Bearer TOKEN'}
payload = {'coupon': 'SINGLE-USE-CODE'}
results = []
barrier = threading.Barrier(20) # Sync 20 threads
def send_request():
barrier.wait() # Wait for all threads
response = requests.post(url, json=payload, headers=headers)
results.append(response.json())
# Create and start 20 threads
threads = [threading.Thread(target=send_request) for _ in range(20)]
for t in threads:
t.start()
for t in threads:
t.join()
# Count successful redemptions
successes = sum(1 for r in results if r.get('success'))
print(f'Successful redemptions: {successes}')Prevention Strategies
Atomic Database Operations
Combine check and update into a single atomic operation:
// Secure: Atomic update with condition
async function redeemCoupon(userId, couponCode) {
// Single atomic query: update only if unused
const result = await db.query(
`UPDATE coupons
SET used = true, used_by = ?, used_at = NOW()
WHERE code = ? AND used = false`,
[userId, couponCode]
);
// Check if any row was actually updated
if (result.affectedRows === 0) {
return { error: 'Invalid or already used coupon' };
}
// Only apply discount if update succeeded
const coupon = await db.query(
'SELECT discount FROM coupons WHERE code = ?',
[couponCode]
);
await applyDiscount(userId, coupon.discount);
return { success: true };
}Database Transactions with Proper Isolation
// Secure: Using transaction with row locking
async function withdrawFunds(userId, amount) {
const connection = await db.getConnection();
try {
await connection.beginTransaction();
// SELECT FOR UPDATE locks the row
const [account] = await connection.query(
'SELECT balance FROM accounts WHERE user_id = ? FOR UPDATE',
[userId]
);
if (account.balance < amount) {
await connection.rollback();
return { error: 'Insufficient funds' };
}
await connection.query(
'UPDATE accounts SET balance = balance - ? WHERE user_id = ?',
[amount, userId]
);
await connection.commit();
return { success: true, newBalance: account.balance - amount };
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}Distributed Locking
For distributed systems, use Redis locks or similar:
import Redis from 'ioredis';
const redis = new Redis();
async function redeemWithLock(userId, couponCode) {
const lockKey = `lock:coupon:${couponCode}`;
const lockValue = `${userId}-${Date.now()}`;
// Try to acquire lock (10 second expiry)
const acquired = await redis.set(
lockKey,
lockValue,
'EX', 10,
'NX'
);
if (!acquired) {
return { error: 'Coupon being processed, try again' };
}
try {
// Safe to process - we have exclusive access
return await processCouponRedemption(userId, couponCode);
} finally {
// Release lock only if we still own it
const script = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
`;
await redis.eval(script, 1, lockKey, lockValue);
}
}Idempotency Keys
Require clients to provide unique idempotency keys:
async function processPayment(req) {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return { error: 'Idempotency-Key header required' };
}
// Check if this request was already processed
const existing = await redis.get(`idem:${idempotencyKey}`);
if (existing) {
return JSON.parse(existing); // Return cached response
}
// Process the payment
const result = await processPaymentInternal(req.body);
// Cache the result for 24 hours
await redis.set(
`idem:${idempotencyKey}`,
JSON.stringify(result),
'EX', 86400
);
return result;
}Security Checklist
- Use atomic database operations for state changes
- Apply SELECT FOR UPDATE or equivalent row locking
- Implement distributed locking for multi-server deployments
- Require idempotency keys for sensitive operations
- Test critical endpoints with concurrent request tools
- Review all check-then-act patterns in business logic
- Use database constraints as a final safety net
- Monitor for duplicate transactions in production
Practice Challenges
View allRace Condition Balance
Withdraw race condition. Overdraft the account.
Time of Check
TOCTOU: Time Of Check, Time Of Use vulnerability.
Database Race
Race condition in database update. Duplicate entries.
File Race
Check file, then use file. Swap it in between.
Session Race
Race condition in session creation. Session fixation.