A race condition occurs when the security or correctness of code depends on the precise timing or order of events across multiple threads or processes, and an…
A race condition occurs when the security or correctness of code depends on the precise timing or order of events across multiple threads or processes, and an attacker can manipulate that timing to cause an unintended outcome. Even a few milliseconds of delay between a security check and the action it protects can be exploited to bypass access controls, corrupt data, or escalate privileges.
02How It Happens
Race conditions arise when two or more execution paths access or modify shared state without proper synchronization. A typical vulnerable pattern is the "check-then-act" antipattern: code verifies a condition (e.g., "does this file exist?" or "is this user authorized?"), then performs an action based on that check. If another thread or process modifies the shared state between the check and the action, the assumption becomes invalid. The window is often tiny, but attackers can force collisions by sending concurrent requests or by exploiting system delays.
03Real-World Impact
Race conditions can lead to privilege escalation (a user gains admin rights by racing a permission update), unauthorized file access (a file is deleted after a permission check but before it's read), data corruption (two processes write to the same resource simultaneously), or authentication bypass (a session is validated and then invalidated before use). The impact depends on what state is being raced; in security-critical contexts, even a narrow window is dangerous.
04Vulnerable & Fixed Patterns
Vulnerable pattern
import os
import time
def process_user_file(user_id, filename):
# Check: does the user own this file?
if not user_owns_file(user_id, filename):
raise PermissionError("Access denied")
# Act: read the file
# (Race window: file could be deleted or reassigned between check and read)
time.sleep(0.1) # Simulates processing delay
with open(filename, 'r') as f:
return f.read()
Why it's vulnerable: The permission check and the file read are separate operations. An attacker can delete the file, change its permissions, or replace it with a symlink between the check and the read.
Fixed pattern
import os
def process_user_file(user_id, filename):
try:
# Open the file first, then verify ownership of the open file descriptor
with open(filename, 'r') as f:
if not user_owns_file_descriptor(user_id, f):
raise PermissionError("Access denied")
return f.read()
except FileNotFoundError:
raise PermissionError("File not found or inaccessible")
Vulnerable pattern
<?php
function transfer_funds($user_id, $amount) {
// Check: does user have sufficient balance?
$balance = get_user_balance($user_id);
if ($balance < $amount) {
return false;
}
// Act: deduct and transfer
// (Race window: another request could also pass the check)
sleep(1); // Simulates processing
update_balance($user_id, $balance - $amount);
return true;
}
?>
Why it's vulnerable: Two concurrent requests can both read the same balance, both pass the check, and both deduct the amount, resulting in a negative balance or double-spending.
Fixed pattern
<?php
function transfer_funds($user_id, $amount) {
global $wpdb;
// Use a database transaction with row-level locking
$wpdb->query('START TRANSACTION');
// Lock the row and check balance atomically
$balance = $wpdb->get_var($wpdb->prepare(
'SELECT balance FROM accounts WHERE user_id = %d FOR UPDATE',
$user_id
));
if ($balance < $amount) {
$wpdb->query('ROLLBACK');
return false;
}
// Deduct within the same transaction
$wpdb->query($wpdb->prepare(
'UPDATE accounts SET balance = balance - %d WHERE user_id = %d',
$amount,
$user_id
));
$wpdb->query('COMMIT');
return true;
}
?>
05Prevention Checklist
Use atomic operations: Combine check-and-act into a single, indivisible database transaction or system call whenever possible.
Apply locks or mutexes: Protect shared state with synchronization primitives (mutexes, semaphores, or database locks) for the entire critical section.
Avoid TOCTOU patterns: Never check a condition on a resource and then act on it separately; instead, open/lock the resource first, then verify within the lock.
Use database transactions: Wrap multi-step operations in transactions with appropriate isolation levels (e.g., FOR UPDATE in SQL) to prevent concurrent modification.
Test with concurrent load: Use stress testing and race-condition detection tools (e.g., ThreadSanitizer, Helgrind) to expose timing-dependent bugs before production.
Document assumptions: Clearly mark code that relies on timing or ordering, and document why it is safe (or why it isn't).
06Signs You May Already Be Affected
Look for unexplained data inconsistencies (balances that don't match, duplicate records, or missing transactions), intermittent permission errors that don't reproduce reliably, or log entries showing the same action performed twice in rapid succession. If you observe behavior that only occurs under high concurrency or load, a race condition may be the culprit.