Skip to main content
Back to blog
April 28, 2026|8 min read

Race Conditions in Web Security: Exploits, Patterns, and Prevention

Race conditions in web applications let attackers exploit the gap between checking a condition and acting on it — enabling double-spending, coupon abuse, and authentication bypass with nothing more than concurrent HTTP requests.

ZeriFlow Team

1,533 words

Race Conditions in Web Security: Exploits, Patterns, and Prevention

A race condition in web security occurs when the behavior of a system depends on the relative timing or ordering of multiple concurrent operations, and that timing can be influenced by an attacker. In web applications, race conditions are exploited by sending multiple simultaneous HTTP requests to trigger operations that should be mutually exclusive — withdrawing more than the account balance, applying a coupon multiple times, or bypassing a one-time-use security token.

Check your web application's security configuration baseline with ZeriFlow — 80+ automated checks, free.


Understanding Race Conditions: The TOCTOU Problem

The most fundamental race condition pattern is TOCTOU: Time of Check to Time of Use. The application checks a condition at one moment (time of check), then acts on it at a later moment (time of use). If the state changes between the check and the use, the check's result is no longer valid.

Thread A: CHECK → balance is $100 → DECISION to approve $100 withdrawal
Thread B: CHECK → balance is $100 → DECISION to approve $100 withdrawal
Thread A: USE → deduct $100 → balance is now $0
Thread B: USE → deduct $100 → balance is now -$100 (INVALID STATE)

The window between check and use — even if it's just milliseconds — is the exploitation window.


Race Condition Attack Patterns in Web Applications

Pattern 1: Double-Spending

Any resource that can only be consumed once is a target: gift cards, promotional codes, one-time passwords, account credits, trial periods.

The attack: The attacker opens 20-50 parallel HTTP connections and fires identical redemption requests simultaneously. Depending on the concurrency model and database isolation level, multiple threads may pass the 'has this code been used?' check before any of them write the 'used = true' flag.

Real-world impact: Significant financial losses in e-commerce platforms. Uber's promo code system was famously exploited via race conditions in bug bounty disclosures.

Pattern 2: Limit Bypass

Applications that limit how many times a user can perform an action (3 free API calls per day, 1 trial per email, max 5 addresses per account) are vulnerable to concurrent requests that all pass the limit check before any increment the counter.

Limit check (5 addresses): 4 current → OK
Limit check (5 addresses): 4 current → OK  (concurrent, same state)
Limit check (5 addresses): 4 current → OK
INSERT address 5 → count = 5
INSERT address 6 → count = 6 (exceeded limit)
INSERT address 7 → count = 7 (exceeded limit)

Pattern 3: Authentication Token Race

Single-use password reset tokens: if the token invalidation and the password change don't happen atomically, an attacker who intercepts a token can race two simultaneous password changes.

Pattern 4: File Upload Race Conditions

An application accepts a file upload, validates it (antivirus scan, type check), and then moves it to permanent storage. Between the upload and the validation, the file exists in a temporary location. An attacker who can predict or control the temporary path can race a symlink attack or replace the file before validation completes.


Web-Specific Race Conditions: The 'Last-Byte Sync' Technique

Modern HTTP/2 connections allow multiple requests in a single TCP connection without Head-of-Line blocking. Burp Suite's 'single-packet attack' (developed by James Kettle) exploits this: all requests are sent in a single TCP packet, ensuring they arrive at the server simultaneously and enter the processing queue at exactly the same moment.

This dramatically increases the effectiveness of race condition exploits compared to HTTP/1.1, where network jitter introduces timing uncertainty.

In HTTP/1.1, the technique is to send partial requests (omit the last few bytes) to multiple connections simultaneously, then simultaneously send the final bytes — synchronizing arrival at the server.


Database-Level Race Conditions

Insufficient Isolation Level

Most databases default to READ COMMITTED isolation. At this level, a transaction can read values committed by other transactions — meaning Thread A can read the balance that Thread B hasn't yet decremented. Using SERIALIZABLE isolation prevents this but impacts performance significantly.

For most applications, REPEATABLE READ with explicit locks on the relevant rows provides the right balance.

Non-Atomic Check-Then-Act

sql
-- VULNERABLE: check and update are two separate operations
SELECT balance FROM accounts WHERE id = 1; -- returns 100
-- ... processing ...
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

Between the SELECT and the UPDATE, another transaction can read and modify the balance.

Atomic Solution

sql
-- SAFE: check and update in a single atomic statement
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;

-- Check affected rows: if 0, the balance was insufficient

Or using SELECT FOR UPDATE:

sql
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- locks the row
-- validate balance here
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

SELECT FOR UPDATE acquires an exclusive lock on the selected rows. Any other transaction attempting to read or update the same row waits until the lock is released, serializing access.


Optimistic Locking

Optimistic locking is a concurrency control strategy where the application assumes conflicts are rare and handles them when they occur, rather than preventing them with locks.

python
# Read the current version
user = db.session.query(User).filter_by(id=user_id).one()
original_version = user.version

# Make changes
user.credits -= 100

# Write back with version check
result = db.session.query(User).filter_by(
    id=user_id,
    version=original_version  # only update if version hasn't changed
).update({
    'credits': user.credits,
    'version': original_version + 1
})

if result == 0:
    raise ConcurrentModificationError('Retry required')

If result == 0, another transaction modified the record between the read and the write. The application retries the operation. This is lighter than pessimistic locking and scales better in read-heavy workloads.


Idempotency Keys

For operations that must execute exactly once despite potential retries or concurrent requests, idempotency keys are the standard solution:

POST /api/payments
Idempotency-Key: client-generated-uuid-abc123
{
    "amount": 100,
    "currency": "USD"
}

The server records each idempotency key with its result. If the same key is received again (retry or concurrent request), the server returns the stored result without re-executing the payment. Keys expire after a defined window (typically 24 hours).

Stripe, Braintree, and most payment APIs implement idempotency keys exactly this way.


Testing for Race Conditions

Using Burp Suite

  1. 1Send the target request to Repeater
  2. 2Create a group of 20-30 identical requests
  3. 3Use 'Send group (parallel)' to fire them simultaneously
  4. 4Look for unexpected successful responses (e.g., multiple success messages for a single-use code)

Using Python (concurrent.futures)

python
import concurrent.futures
import requests

def redeem_coupon(session_token):
    return requests.post(
        'https://target.com/api/redeem',
        json={'code': 'PROMO50'},
        headers={'Authorization': f'Bearer {session_token}'}
    )

with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
    futures = [executor.submit(redeem_coupon, TOKEN) for _ in range(20)]
    results = [f.result() for f in futures]

successes = [r for r in results if r.status_code == 200]
print(f'{len(successes)} successful redemptions')  # Should be 1, vulnerability if >1

FAQ

Q: Are race conditions only exploitable by sophisticated attackers?

A: No. Modern tools like Burp Suite make race condition testing straightforward, and the 'single-packet attack' technique removes most of the timing uncertainty that made exploitation difficult historically. Race conditions are now routinely discovered and exploited by security researchers and attackers alike.

Q: Does rate limiting prevent race condition exploitation?

A: Rate limiting reduces the window but doesn't eliminate the vulnerability. An attacker sending 50 concurrent requests may still fall within rate limits (50 requests per second is common). Atomic operations at the database level are the correct fix; rate limiting is a partial mitigation.

Q: What's the difference between a race condition and a TOCTOU vulnerability?

A: TOCTOU (Time of Check to Time of Use) is the most common pattern of race condition in web security. All TOCTOU vulnerabilities are race conditions, but race conditions encompass a broader category including ordering-dependent state machines and non-atomic compound operations.

Q: Can NoSQL databases like MongoDB have race conditions?

A: Yes. MongoDB's default findAndModify operation is atomic, which prevents simple race conditions. But sequences of separate reads and writes are not atomic. MongoDB provides findOneAndUpdate with returnNewDocument for atomic check-and-update patterns, and supports multi-document transactions for complex atomic operations.

Q: Is optimistic or pessimistic locking better?

A: It depends on your workload. Optimistic locking performs better when conflicts are rare (reads >> writes, low contention). Pessimistic locking with SELECT FOR UPDATE is safer when conflicts are common (high contention on the same rows) because it avoids retry loops. Most e-commerce applications with discount/coupon logic benefit from pessimistic locking on those specific operations.


Conclusion

Race conditions in web applications are exploitable, practically impactful, and often underestimated. The TOCTOU pattern — check, then act, with a gap in between — is everywhere in application code, and closing that gap requires moving to atomic database operations, optimistic locking, idempotency keys, or appropriate transaction isolation.

Building a strong security baseline is the first step to a comprehensive security posture.

Scan your application with ZeriFlow to identify configuration issues and security gaps before they become exploitation vectors.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading