Web Security

Race Condition Vulnerabilities in Web Applications

Race conditions in web applications lead to double-spending, privilege escalation, and data corruption. This guide covers the most common patterns, detection techniques, and practical defenses.

Michael
Security Researcher
7 min read

Race conditions are the dark horse of web vulnerabilities. They do not show up in static analysis. They do not appear in single-threaded testing. They require precise timing and concurrent requests. And yet, when exploited, they can drain accounts, bypass security controls, and corrupt critical data.

A race condition occurs when the outcome of an operation depends on the timing of concurrent events. In web applications, this typically means two or more requests processing simultaneously and interacting with shared state in unintended ways. The application assumes sequential processing, but HTTP requests arrive in parallel.

The Double-Spend Problem

The most financially impactful race condition is the double-spend. A user has $100 in their account and sends two simultaneous transfer requests for $100 each. Both requests read the balance as $100, both verify the balance is sufficient, and both deduct $100. The user transfers $200 from a $100 balance.

This pattern affects any operation that follows a check-then-act sequence:

  1. Read the current state (check balance)
  2. Validate the state (balance is greater than or equal to transfer amount)
  3. Modify the state (deduct transfer amount)

If two requests execute steps 1 and 2 before either reaches step 3, both pass validation based on the same stale state.

Real-world examples:

  • Gift card redemption: redeem the same card twice simultaneously
  • Coupon application: apply a single-use coupon to multiple orders
  • Reward points: spend the same points on multiple purchases
  • Limited inventory: purchase the last item in stock multiple times

Prevention:

  • Use database transactions with SELECT ... FOR UPDATE to lock the row during the check-then-act sequence
  • Implement optimistic locking with version numbers or timestamps
  • Use atomic operations where possible (e.g., UPDATE accounts SET balance = balance - 100 WHERE balance >= 100)
  • Idempotency keys that prevent duplicate processing of the same request

Limit Bypass Attacks

Applications enforce various limits -- rate limits, invitation limits, vote limits, trial account limits. Race conditions can bypass these:

Rate limit bypass. A rate limiter allows 10 requests per minute. The attacker sends 100 simultaneous requests. If the rate limiter reads the counter, increments it, and checks against the limit in separate operations, multiple requests can read the same counter value and all pass the check.

Vote manipulation. A user can vote once per poll. Two simultaneous vote requests both check that the user has not voted, both find no existing vote, and both insert a vote.

Free trial abuse. Account creation is limited to one per email. Two simultaneous registration requests with the same email both check for existing accounts, both find none, and both create accounts.

Prevention:

  • Use unique database constraints to enforce uniqueness at the data layer
  • Implement rate limiting with atomic increment operations (Redis INCR, for example)
  • Use advisory locks or mutex mechanisms for critical sections
  • Design limits to be enforced at the storage layer, not the application layer

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

TOCTOU is the general pattern behind most race conditions. The application checks a condition at one point in time (time-of-check) and uses the result at a later point (time-of-use). If the condition changes between check and use, the application behaves incorrectly.

File system TOCTOU:

  1. Application checks if file exists and is owned by the user
  2. Attacker replaces the file with a symlink to a sensitive file
  3. Application reads the file (now the symlink target)

Permission TOCTOU:

  1. Application checks if user has permission to perform action
  2. Admin revokes the user's permission
  3. Application performs the action (using the stale permission check)

State TOCTOU:

  1. Application checks order status is "pending"
  2. Another process changes status to "cancelled"
  3. Application processes payment for the cancelled order

Prevention:

  • Minimize the time between check and use
  • Re-verify conditions at the point of use, not just at the point of check
  • Use file operations that combine check and use atomically (O_CREAT|O_EXCL for file creation)
  • Design state machines with atomic transitions

Exploiting Race Conditions

Exploiting race conditions requires sending multiple requests that arrive at the server simultaneously. Several techniques make this practical:

HTTP/2 single-packet attack. HTTP/2 multiplexes multiple requests over a single TCP connection. By sending multiple requests in a single TCP packet, they arrive at the server at the same instant, maximizing the chance of triggering the race condition.

Last-byte synchronization. For HTTP/1.1, send all requests except the last byte of each body, then send all final bytes simultaneously. The server receives all complete requests at nearly the same time.

Turbo Intruder. Burp Suite's Turbo Intruder extension is specifically designed for race condition exploitation. It can send hundreds of concurrent requests with minimal timing variation.

Detection signals:

  • Unexpected duplicate records in the database
  • Negative balances or quantities that should never be negative
  • Constraint violation errors in logs
  • Inconsistent audit trails showing impossible sequences of events

Database-Level Defenses

The database is your strongest ally against race conditions:

Pessimistic locking. SELECT ... FOR UPDATE locks the selected rows until the transaction commits. Other transactions attempting to read or modify the same rows will wait.

BEGIN;
SELECT balance FROM accounts WHERE id = 123 FOR UPDATE;
-- Check balance and deduct
UPDATE accounts SET balance = balance - 100 WHERE id = 123;
COMMIT;

Optimistic locking. Add a version column to your tables. Read the version with the data, and include it in the update condition. If another transaction modified the row, the version will not match, and the update affects zero rows.

UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 123 AND version = 5;
-- If affected_rows == 0, someone else modified the row

Unique constraints. For operations that should happen only once, use unique constraints in the database. A unique constraint on (user_id, poll_id) in the votes table prevents double voting regardless of race conditions.

Atomic operations. Combine the check and modification into a single statement. UPDATE accounts SET balance = balance - 100 WHERE id = 123 AND balance >= 100 is atomic -- it either succeeds or fails, with no window for a race condition.

Application-Level Defenses

Idempotency keys. Require clients to include a unique idempotency key with each request. Store the key on first processing, and return the cached result for duplicate keys. This prevents duplicate processing regardless of timing.

Distributed locks. For operations spanning multiple services, use distributed locking mechanisms (Redis SETNX, Zookeeper, etcd). Acquire the lock before the check-then-act sequence and release it after.

Queue-based processing. Instead of processing requests synchronously, enqueue them and process sequentially. A single worker processing requests from a queue cannot have race conditions because requests are processed one at a time.

State machines. Model business processes as explicit state machines with defined transitions. Validate state transitions atomically. An order in "shipped" state cannot transition to "cancelled" if the state machine does not allow it.

Testing for Race Conditions

Race conditions are notoriously difficult to test because they are timing-dependent. Strategies include:

  • Concurrent test frameworks. Write tests that spawn multiple threads or coroutines executing the same operation simultaneously.
  • Deliberate delays. Add artificial delays between the check and act phases during testing to widen the race window.
  • Stress testing. Run load tests with the specific goal of triggering race conditions in critical operations.
  • Property-based testing. Use property-based testing frameworks to generate random sequences of concurrent operations and verify invariants hold.

How Safeguard.sh Helps

Safeguard.sh helps mitigate race condition risks by monitoring the concurrency-related vulnerabilities in your dependency stack. Database drivers, ORM libraries, and web frameworks all have histories of concurrency bugs that can introduce race conditions even when your application logic is correct. Safeguard.sh tracks CVEs across your entire SBOM and alerts when a component in your stack has a known concurrency vulnerability, giving your team the visibility to patch before the timing works in an attacker's favor.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.