5b9f30e663
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.
206 lines
5.9 KiB
Markdown
206 lines
5.9 KiB
Markdown
# 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](https://cwe.mitre.org/data/definitions/362.html)
|
|
|
|
## 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|