Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Academy Race Conditions & TOCTOU
Advanced · 20 min

Race Conditions & TOCTOU

Discover how concurrent requests can bypass security checks and drain accounts — and how atomic operations and locks stop them.

1 What are Race Conditions?

A race condition occurs when the outcome of a program depends on the relative timing of two or more concurrent operations. In security contexts, an attacker deliberately triggers a window between a check and the subsequent action to gain an unintended advantage.

Consider a bank transfer endpoint that checks the balance then deducts it:

def transfer(user_id, amount):
    balance = db.get_balance(user_id)   # READ
    if balance >= amount:
        db.set_balance(user_id, balance - amount)  # WRITE
        send_money(amount)

If two requests arrive simultaneously, both may read the same balance value (e.g. $100), both pass the check, and both deduct $80 — leaving the account at -$60. This is called a double-spend vulnerability.

Real-world impact: Race conditions have been exploited to overdraw bank accounts, redeem the same coupon multiple times, claim bonus credits repeatedly, and bypass rate limits.

2 TOCTOU — Time-of-Check to Time-of-Use

TOCTOU (Time-of-Check to Time-of-Use) is a specific race condition pattern where the state of a resource changes between the check and the use of that resource.

File system TOCTOU example:

import os

def process_upload(path):
    # CHECK: is the file safe?
    if not os.path.islink(path) and os.access(path, os.R_OK):
        # TIME GAP — attacker swaps file for a symlink here
        with open(path) as f:   # USE: now reads /etc/shadow
            return f.read()

Between the os.access() call and the open(), an attacker with local access can replace the file with a symlink to /etc/shadow. The check passed on the original file; the open acts on the symlink.

Database TOCTOU: Similarly, checking a row value in one query and acting on it in a second query creates a gap. Another transaction can modify the row between the two operations.

-- Vulnerable: two separate queries
SELECT points FROM users WHERE id = ?;   -- check
UPDATE users SET points = points - 100 WHERE id = ?;  -- use

3 Fixing with Atomic Operations & Locks

The fundamental fix is to eliminate the gap between check and use — make the operation atomic.

Database transactions with row-level locking:

from contextlib import contextmanager

def transfer(conn, from_id, to_id, amount):
    with conn:  # transaction — commits or rolls back
        # SELECT FOR UPDATE locks the row until commit
        row = conn.execute(
            "SELECT balance FROM accounts WHERE id = ? FOR UPDATE",
            (from_id,)
        ).fetchone()
        if row['balance'] < amount:
            raise ValueError("Insufficient funds")
        conn.execute(
            "UPDATE accounts SET balance = balance - ? WHERE id = ?",
            (amount, from_id)
        )
        conn.execute(
            "UPDATE accounts SET balance = balance + ? WHERE id = ?",
            (amount, to_id)
        )

Atomic SQL UPDATE with condition:

-- Atomic check-and-debit in one statement
UPDATE accounts
SET balance = balance - :amount
WHERE id = :user_id AND balance >= :amount;
-- Check affected rows == 1; if 0, the balance was insufficient

Mutexes in application code (Python threading):

import threading

_locks = {}

def get_user_lock(user_id):
    if user_id not in _locks:
        _locks[user_id] = threading.Lock()
    return _locks[user_id]

def withdraw(user_id, amount):
    with get_user_lock(user_id):
        balance = get_balance(user_id)
        if balance >= amount:
            set_balance(user_id, balance - amount)

Atomic compare-and-swap (Redis): Use Redis WATCH / MULTI / EXEC for distributed systems where multiple servers share state. Only one transaction will succeed when the watched key changes.

Knowledge Check

0/4 correct
Q1

An e-commerce site lets users apply a coupon code. The code checks if the coupon is unused, then marks it used in a separate step. What is the vulnerability?

Q2

What does TOCTOU stand for, and why is it dangerous?

Q3

Which SQL pattern eliminates a race condition on a balance check?

Q4

Which mechanism should you use in a distributed system (multiple app servers) to prevent race conditions?

Code Exercise

Fix the Bank Transfer Race Condition

The transfer() function below reads the balance, checks it, then updates it in two separate steps — vulnerable to double-spend race conditions. Fix it using a database transaction with an atomic UPDATE that includes the balance check in the WHERE clause.

python