#
Race Conditions (Time-of-Check to Time-of-Use)
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?
Critical Timing Vulnerability
Race conditions can lead to financial losses, duplicate transactions, coupon fraud, and inventory manipulation. They're particularly dangerous because they're difficult to detect and can be exploited at scale with automated tools.
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:
- Create 2 gift cards: A ($5) and B ($0)
- Send 100 simultaneous requests to transfer $5 from A to B
- All requests check A has $5 → All succeed
- 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
Critical Points
- Always use transactions: Database transactions prevent race conditions
- Lock critical resources: Use SELECT FOR UPDATE or distributed locks
- Atomic operations: Use Redis INCR, not read-then-write
- Rate limit sensitive endpoints: Prevent mass concurrent requests
- Test with concurrency: Use tools to simulate concurrent requests
- Idempotency keys: Prevent duplicate operations
- Monitor and alert: Detect unusual concurrent activity patterns
- Business logic protection: Critical operations need strongest protection
Last Updated: November 2025