Files
security-patterns/race-conditions.md
T
Rodin 5b9f30e663 Add SSRF, race conditions, JWT security patterns
High-priority patterns from completeness review:
- ssrf.md: metadata endpoints, DNS rebinding, webhook validation
- race-conditions.md: TOCTOU, atomic operations, file/db races
- jwt-security.md: algorithm confusion, kid injection, refresh tokens

Now 16 patterns covering comprehensive web application security.
2026-05-10 23:17:54 -07:00

5.9 KiB

Race Conditions and TOCTOU

Rule

Check-then-act must be atomic. Never trust state between check and use.

Source: CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization

TOCTOU (Time-of-Check to Time-of-Use)

Thread A: check(x)     -->    use(x)
Thread B:        modify(x)
                   ^-- state changes between check and use

Correct Pattern

import threading
from contextlib import contextmanager

# Pattern 1: Atomic check-and-act with locking
class BankAccount:
    def __init__(self, balance: Decimal):
        self.balance = balance
        self._lock = threading.Lock()
    
    def withdraw(self, amount: Decimal) -> bool:
        """Atomic withdrawal - no race window."""
        with self._lock:
            if self.balance >= amount:
                self.balance -= amount
                return True
            return False

# Pattern 2: Database-level atomicity
def transfer_funds(conn, from_id: int, to_id: int, amount: Decimal):
    """Use database transaction + row locks."""
    with conn.begin():
        # SELECT FOR UPDATE prevents concurrent modification
        from_acct = conn.execute(
            "SELECT balance FROM accounts WHERE id = %s FOR UPDATE",
            (from_id,)
        ).fetchone()
        
        if from_acct.balance < amount:
            raise InsufficientFunds()
        
        conn.execute(
            "UPDATE accounts SET balance = balance - %s WHERE id = %s",
            (amount, from_id)
        )
        conn.execute(
            "UPDATE accounts SET balance = balance + %s WHERE id = %s",
            (amount, to_id)
        )

# Pattern 3: Compare-and-swap (optimistic locking)
def update_with_version(conn, item_id: int, new_data: dict, expected_version: int):
    """Fail if version changed since we read it."""
    result = conn.execute(
        """UPDATE items 
           SET data = %s, version = version + 1 
           WHERE id = %s AND version = %s""",
        (new_data, item_id, expected_version)
    )
    if result.rowcount == 0:
        raise ConcurrentModificationError("Item was modified by another request")

Incorrect Pattern

# Wrong: check-then-act without atomicity
class BankAccount:
    def withdraw(self, amount):
        if self.balance >= amount:  # Check
            # Race window! Another thread can withdraw here
            self.balance -= amount   # Act
            return True
        return False

# Wrong: file race condition
def safe_write(path, data):
    if not os.path.exists(path):  # Check
        # Race window! File could be created here
        with open(path, 'w') as f:  # Act
            f.write(data)

# Wrong: double-checked locking (broken in many languages)
_instance = None
_lock = threading.Lock()

def get_instance():
    if _instance is None:  # First check without lock
        with _lock:
            if _instance is None:  # Second check
                _instance = ExpensiveObject()
    return _instance

File System Races

import os
import tempfile

# Wrong: check then create
def create_file(path):
    if os.path.exists(path):
        raise FileExistsError()
    with open(path, 'w') as f:  # Race!
        f.write("data")

# Correct: atomic creation (fails if exists)
def create_file_safe(path):
    fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
    try:
        os.write(fd, b"data")
    finally:
        os.close(fd)

# Wrong: temp file with predictable name
def bad_temp():
    path = f"/tmp/myapp_{os.getpid()}.tmp"  # Predictable!
    with open(path, 'w') as f:
        f.write(secret_data)

# Correct: secure temp file
def good_temp():
    fd, path = tempfile.mkstemp()
    try:
        os.write(fd, secret_data.encode())
    finally:
        os.close(fd)
        os.unlink(path)

Signup / Registration Races

# Wrong: check username then create
def register(username: str, password: str):
    if User.query.filter_by(username=username).first():
        raise UsernameExists()
    # Race window! Another request could register same username
    user = User(username=username, password=hash(password))
    db.session.add(user)
    db.session.commit()

# Correct: use database constraint, handle exception
def register_safe(username: str, password: str):
    user = User(username=username, password=hash(password))
    db.session.add(user)
    try:
        db.session.commit()  # UNIQUE constraint enforced here
    except IntegrityError:
        db.session.rollback()
        raise UsernameExists()

Coupon / Discount Races

# Wrong: check-then-apply coupon
def apply_coupon(order_id: int, coupon_code: str):
    coupon = Coupon.query.filter_by(code=coupon_code).first()
    if coupon.uses_remaining <= 0:
        raise CouponExhausted()
    
    # Race window! 100 requests could pass the check simultaneously
    order = Order.query.get(order_id)
    order.discount = coupon.discount
    coupon.uses_remaining -= 1
    db.session.commit()

# Correct: atomic decrement with row lock
def apply_coupon_safe(order_id: int, coupon_code: str):
    with db.session.begin():
        result = db.session.execute(
            """UPDATE coupons 
               SET uses_remaining = uses_remaining - 1 
               WHERE code = :code AND uses_remaining > 0
               RETURNING discount""",
            {"code": coupon_code}
        )
        row = result.fetchone()
        if not row:
            raise CouponExhausted()
        
        db.session.execute(
            "UPDATE orders SET discount = :discount WHERE id = :id",
            {"discount": row.discount, "id": order_id}
        )

Edge Cases

  • Rate limiters with race conditions allow bursts
  • Session creation races can create duplicates
  • Inventory/stock decrements need atomic operations
  • Distributed systems need distributed locks (Redis, etcd)
  • File permission checks before open (symlink attacks)
  • Signal handlers can interrupt between check and use