Documentation

# Race Conditions (Time-of-Check to Time-of-Use)

Race Conditions Illustration
Race Conditions Illustration

A timing vulnerability where attackers exploit the gap between security checks and resource access, allowing them to bypass limits, make duplicate purchases, or exploit business logic flaws through concurrent requests.

MEDIUM SEVERITY BUSINESS LOGIC CONCURRENCY


# What are Race Conditions?

In Simple Terms:

Imagine a bank vault with $1000. The security guard checks: "You have $1000, so you can withdraw $800." But while the guard is processing your withdrawal, you send 10 friends to simultaneously withdraw $800 each.

All 10 requests pass the check (balance is still $1000 when checked), and all 10 withdrawals process—you just withdrew $8000 from an account with only $1000!

This is a race condition: exploiting the time gap between checking (validation) and acting (execution).


# How Race Conditions Work

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

1. Check: User has $1000 balance
2. Wait: Process payment
3. Use: Deduct $800
4. Final: Balance = $200
Thread 1: Check ($1000) → Wait → Deduct $800
Thread 2: Check ($1000) → Wait → Deduct $800  (simultaneous!)
Thread 3: Check ($1000) → Wait → Deduct $800  (simultaneous!)

All pass check, all deduct from same $1000!
Result: Balance = -$1400 (overdraft!)

# Types of Race Condition Attacks

# 1. Multiple Coupon/Discount Usage

FINANCIAL LOSS

Vulnerable Code:

@app.route('/apply-coupon', methods=['POST'])
def apply_coupon():
    coupon_code = request.json['coupon']
    user_id = session['user_id']
    
    # Check if coupon already used
    if db.coupon_used(user_id, coupon_code):
        return {"error": "Coupon already used"}, 400
    
    # Apply 50% discount
    discount = calculate_discount(coupon_code)
    
    # Mark as used (RACE CONDITION HERE!)
    db.mark_coupon_used(user_id, coupon_code)
    
    return {"discount": discount}

Attack: Send 100 simultaneous requests → All pass the check → Apply coupon 100 times!

# 2. Balance/Credit Manipulation

CRITICAL

Vulnerable Withdrawal:

def withdraw(user_id, amount):
    balance = db.get_balance(user_id)
    
    # TOCTOU vulnerability
    if balance >= amount:
        time.sleep(0.1)  # Simulating processing delay
        db.set_balance(user_id, balance - amount)
        return True
    
    return False

Attack: 10 simultaneous withdrawals of $900 from $1000 account → All succeed!

# 3. Limited Item Purchase

INVENTORY FRAUD

Vulnerable Stock Check:

@app.route('/buy-limited-item', methods=['POST'])
def buy_item():
    item_id = request.json['item_id']
    
    # Check stock
    stock = db.get_stock(item_id)
    if stock <= 0:
        return {"error": "Out of stock"}, 400
    
    # Process payment (takes time)
    process_payment()
    
    # Decrease stock
    db.decrease_stock(item_id, 1)
    
    return {"success": True}

Attack: Limited sneaker release (1 item) → 1000 simultaneous requests → 1000 purchases!


# Real-World Examples

# Case Study 1: Starbucks Gift Card Race Condition (2015)

Vulnerability: Transfer funds between gift cards without proper locking

Attack:

  1. Create 2 gift cards: A ($5) and B ($0)
  2. Send 100 simultaneous requests to transfer $5 from A to B
  3. All requests check A has $5 → All succeed
  4. Result: B has $500, A has -$495

Impact:

  • $100,000+ stolen
  • Fixed with database locking
  • Users who exploited faced legal action

# Case Study 2: E-Commerce Flash Sale Exploitation (2019)

Vulnerability: Limited quantity check without atomic operations

Attack:

# Vulnerable code
def purchase_limited_item():
    sold = redis.get('sold_count')
    if sold < 100:  # Only 100 available
        # Process payment (slow)
        charge_credit_card()
        # Increment counter
        redis.incr('sold_count')

Exploit: Attacker used 1000 concurrent requests → Purchased 1000 items despite 100 limit

Impact:

  • $2 million in losses
  • Had to honor all purchases at discounted price
  • Reputation damage

# Case Study 3: Banking App Double Spend (2021)

Vulnerability: Mobile banking transfer without transaction isolation

Attack: User initiated transfer of $10,000 from checking to savings 50 times simultaneously

Result:

  • Checking: -$490,000
  • Savings: +$500,000
  • Net gain: $10,000 (!)

Impact:

  • $5 million stolen by multiple users
  • Required federal investigation
  • Bank had to compensate affected accounts

# Prevention Strategies

# 1. Database Transactions with Locking

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

def safe_withdraw(user_id, amount):
    session = Session()
    try:
        # SELECT FOR UPDATE locks the row
        user = session.query(User).filter_by(id=user_id).with_for_update().first()
        
        if user.balance >= amount:
            user.balance -= amount
            session.commit()
            return True
        else:
            session.rollback()
            return False
    except Exception as e:
        session.rollback()
        raise e
    finally:
        session.close()

# 2. Atomic Operations with Redis

import redis

r = redis.Redis()

def apply_coupon_safe(user_id, coupon_code):
    key = f"coupon:{coupon_code}:user:{user_id}"
    
    # Atomic SET if not exists
    result = r.set(key, 1, nx=True, ex=3600)
    
    if result:
        # Coupon applied successfully
        return calculate_discount()
    else:
        # Already used
        return None

# 3. Optimistic Locking

def update_with_version(item_id, new_quantity):
    while True:
        item = db.get_item(item_id)
        old_version = item.version
        
        # Update with version check
        affected = db.execute("""
            UPDATE items 
            SET quantity = ?, version = version + 1
            WHERE id = ? AND version = ?
        """, [new_quantity, item_id, old_version])
        
        if affected > 0:
            return True  # Success
        else:
            continue  # Retry - someone else modified it

# 4. Rate Limiting

from functools import wraps
from flask import request
import time

user_request_times = {}

def rate_limit(max_per_second=1):
    def decorator(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            user_id = session.get('user_id')
            now = time.time()
            
            if user_id in user_request_times:
                time_since_last = now - user_request_times[user_id]
                if time_since_last < (1.0 / max_per_second):
                    return {"error": "Rate limit exceeded"}, 429
            
            user_request_times[user_id] = now
            return f(*args, **kwargs)
        return wrapped
    return decorator

@app.route('/buy-item', methods=['POST'])
@rate_limit(max_per_second=1)
def buy_item():
    # Now limited to 1 request per second per user
    pass

# Security Checklist

  • Use database transactions with proper isolation
  • Implement row-level locking (SELECT FOR UPDATE)
  • Use atomic operations (Redis INCR, SETNX)
  • Implement optimistic locking with version numbers
  • Add rate limiting per user/IP
  • Use idempotency keys for critical operations
  • Test with concurrent requests
  • Monitor for suspicious patterns
  • Implement request deduplication
  • Use distributed locks for multi-server setups

# Key Takeaways


Last Updated: November 2025