Race conditions (CWE-362) occur when code doesn't properly synchronize access to shared resources, allowing multiple concurrent operations to interfere with each other. The most common variant is Time-of-Check-Time-of-Use (TOCTOU), where a check is performed, then a resource is used, but the resource changes between the check and use.
Real-World Attack Scenarios
Scenario 1: TOCTOU in File Access
A file operation checks ownership then accesses the file:
defdelete_file(filename):# CHECK: Verify user owns fileifnotuser_owns_file(filename):raisePermissionDenied()# TIME WINDOW - Attacker can act here!# USE: Delete the file os.remove(filename)
The vulnerability:
Between check and use, attacker can modify the file (symlink attack):
# Create initial file
echo "data" > /home/user/myfile.txt
# In a loop, delete the file and replace with symlink to target
while true; do
rm /home/user/myfile.txt
ln -sf /etc/shadow /home/user/myfile.txt
done
# Simultaneously, trigger application to delete file
# App checks ownership (passes), deletes file
# But file is now symlink to /etc/shadow
# /etc/shadow gets deleted!
def withdraw(user_id, amount):
# CHECK: Get user's balance
balance = get_balance(user_id)
if balance < amount:
raise InsufficientFunds()
# TIME WINDOW - Balance can change here!
# USE: Perform withdrawal
new_balance = balance - amount
set_balance(user_id, new_balance)
return new_balance
User has $100
Thread 1 (Withdrawal A - $100):
1. Check: balance = get_balance() → $100
2. Check: $100 >= $100 ✓
3. [Context switch]
Thread 2 (Withdrawal B - $100):
4. Check: balance = get_balance() → $100
5. Check: $100 >= $100 ✓
6. set_balance($0)
7. [Context switch]
Thread 1 continues:
8. set_balance($0) # Should be $0, but computed as $100-$100=$0
Result: Both withdrawals succeed!
User withdrew $200 with $100 balance!
# Trigger two withdrawals simultaneously
curl http://bank.com/withdraw?amount=100 &
curl http://bank.com/withdraw?amount=100 &
wait
# Both succeed despite insufficient balance
def promote_to_admin(user_id):
# CHECK: Verify user is moderator
user = get_user(user_id)
if user.role != 'moderator':
raise PermissionDenied()
# TIME WINDOW - User role can change!
# USE: Promote to admin
user.role = 'admin'
save_user(user)
Thread 1 (Admin promotion):
1. Check: user.role = 'moderator' ✓
2. [Context switch]
Thread 2 (Attacker - different session):
3. Somehow change own role to 'moderator' (exploit)
4. Then: admin_promote_to_admin(my_id)
Thread 1 continues:
5. Check still passes (moderator when checked)
6. Promote attacker to admin!
def delete_account(user_id):
# ACTION: Delete account
database.delete_user(user_id)
# CHECK: Verify user had permission (AFTER!)
user = get_user_from_cache(user_id) # Outdated cache
if not user.can_delete_self:
# Too late, already deleted!
raise PermissionDenied()
def purchase_item(item_id, quantity):
# CHECK: Get stock
stock = get_stock(item_id)
if stock < quantity:
raise OutOfStock()
# TIME WINDOW - Stock can change!
# USE: Sell items
new_stock = stock - quantity
set_stock(item_id, new_stock)
return new_stock
Item has 10 units in stock
Customer A (buy 8):
1. Check: stock = 10 ✓
2. [Context switch]
Customer B (buy 5):
3. Check: stock = 10 ✓
4. set_stock(5) # 10 - 5
Customer A continues:
5. set_stock(2) # 10 - 8
RESULT: Sold 13 units from 10 in stock!
import threading
import requests
def withdraw():
requests.post('http://bank.com/withdraw', {'amount': 100})
# Create 5 concurrent threads
threads = [threading.Thread(target=withdraw) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
# Check result - should fail but might succeed
# Add debugging/logging to increase timing window
# This increases likelihood of hitting race condition
-- Bad: Multiple separate queries
SELECT balance FROM users WHERE id=1;
-- ... compute ...
UPDATE users SET balance=new_balance WHERE id=1;
-- Good: Atomic transaction
BEGIN TRANSACTION;
UPDATE users SET balance = balance - amount WHERE id=1;
COMMIT;
# Bad
if balance >= amount:
new_balance = balance - amount
set_balance(new_balance)
# Good - Atomic operation
def withdraw_safe(user_id, amount):
# Single atomic operation
result = database.execute("""
UPDATE accounts
SET balance = balance - ?
WHERE id = ? AND balance >= ?
RETURNING balance
""", [amount, user_id, amount])
if not result:
raise InsufficientFunds()
return result[0]
with database.transaction():
balance = get_balance(user_id)
if balance < amount:
raise InsufficientFunds()
set_balance(user_id, balance - amount)
import threading
balance_lock = threading.Lock()
def withdraw(amount):
with balance_lock: # Acquire lock
balance = get_balance()
if balance < amount:
raise InsufficientFunds()
set_balance(balance - amount)
# Lock released
def update_user(user_id, data, version):
# Only update if version matches
result = database.execute("""
UPDATE users
SET data = ?, version = version + 1
WHERE id = ? AND version = ?
""", [data, user_id, version])
if not result:
raise VersionMismatch()
# Don't follow symlinks
os.remove(filename, follow_symlinks=False)
# Or use secure temp files
import tempfile
with tempfile.NamedTemporaryFile(delete=False) as f:
f.write(data)
f.flush()
# File is secure