Skip to content

CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')

Overview

A race condition occurs when two or more threads, processes, or requests access the same shared resource - a counter, an account balance, a file, a database row, a cache entry, a session - and the outcome depends on the order in which their operations interleave. When a read-modify-write or check-then-act sequence on that resource is not made atomic, two concurrent operations can both read the same starting state, both proceed as if they were the only one acting, and one update can silently overwrite the other. The common shape in a web application is two simultaneous requests hitting the same "check balance, then deduct" or "check inventory, then decrement" code path, without a lock or database-level guarantee holding the sequence together.

Relationship to Other CWEs

Three pages cover race conditions - the general weakness, and two narrower patterns:

  • CWE-362 (this page) - any shared resource accessed by more than one thread, process, or request without adequate synchronization.
  • CWE-367 (Time-of-check Time-of-use) - a single value is checked, then used in a separate step, and the underlying state changes in between: a file is replaced with a symlink after a permission check, or a balance changes after a check but before a debit. Every TOCTOU flaw is a race condition, but CWE-362 also covers races that are not a check followed by a use - two concurrent increments to the same counter, for example, involve no "check" at all.
  • CWE-366 (Race Condition within a Thread) - unsynchronized state shared only among threads inside a single process. CWE-362 is broader: it also covers races across separate processes, servers, or application instances, where the shared resource is a database row or a shared file rather than in-memory state, and where the fix has to live at the datastore rather than in an in-process lock.

A finding reported simply as a "race condition" or "TOCTOU" belongs on whichever page matches the pattern in the code. When it does not clearly match CWE-367 or CWE-366, start here.

OWASP Classification

A06:2025 - Insecure Design

Risk

High: Unsynchronized access to a shared resource lets an attacker send concurrent requests to produce an outcome the application never intended: withdrawing more money than an account holds (double-spending), redeeming a discount code or free-trial offer multiple times, oversubscribing limited inventory, or racing a permission change to have a privileged action execute after the permission was revoked. In multi-tenant or financial systems, the impact is direct data corruption or financial loss, not just an inconsistent read - the wrong final state is written back and persists.

Remediation Steps

Core Principle: Treat the shared resource's entire read-modify-write or check-then-act sequence as a single atomic unit - synchronizing only part of the sequence still leaves the race window open.

Trace the Data Path

  • Source: The shared resource being read - an in-memory variable, object field, or collection; or a persisted resource such as a database row, file, cache key, or session.
  • Sink: The point where a decision is made from that read (a balance or inventory check, a permission check, an existence check) and the value is written back or the decision is acted on.
  • Data Flow / Missing Controls: What else - another thread, another process, another request - can read or write the same resource during the interval between the read and the write. Confirm whether any lock, mutex, or transaction covers the full interval, or only part of it.

Make In-Process Access Atomic (Primary Defense)

When the resource lives in memory within a single process, serialize access to it:

  • Wrap the full critical section - from the first read of the shared state through the final write - in an in-process lock, mutex, or semaphore.
  • For a single variable (a counter, a flag), prefer an atomic primitive over a lock; it avoids the cost and deadlock risk of explicit locking.
  • Never assume a runtime is single-threaded, or that a request-scoped object cannot be shared, without verifying that assumption for the specific server, framework, or concurrency model in use.

Push Atomicity to the Datastore for Cross-Process State (Primary Defense)

When the resource is shared across processes, servers, or application instances - which is the common case for a web application's database rows, files, or cache entries - an in-process lock cannot help, because it only protects one process. Move the atomicity into the datastore itself:

  • Use a row-level lock (a SELECT ... FOR UPDATE-style read inside a transaction) so the check and the write happen under a lock the database enforces across every process.
  • Prefer a single, conditional atomic statement over a separate read then write, for example an update that both applies the change and re-checks the precondition in one statement, rejecting the write if the precondition no longer holds.
  • Use a unique constraint to make a duplicate concurrent insert fail outright, instead of relying on an application-level existence check.
  • Use optimistic locking with a version or timestamp column when contention is expected to be rare: read the version along with the data, and make the write conditional on the version being unchanged, retrying or rejecting on conflict.

Add Conflict Detection as Defense in Depth

For operations that genuinely cannot be fully serialized (for example, a multi-step workflow spanning several requests), add controls that detect and reject a conflicting concurrent attempt rather than silently accepting it:

  • Idempotency keys so a retried or duplicated request cannot be applied twice.
  • Explicit conflict responses (a rejected write, a "state changed, please retry" result) instead of last-write-wins.
  • Never rely on client-side throttling, disabled buttons, or request timing to prevent concurrent access - these are trivially bypassed and enforce nothing server-side.

Harden Configuration and Apply Consistently

  • Apply the same lock, mutex, or transaction isolation level on every code path that touches the resource, including error-handling and retry paths - a race fixed on the happy path but not on a retry path is still exploitable.
  • Keep the critical section minimal but complete: acquire before the first read of the shared state, release only after the final write.

Test for the Race Condition

  • Drive genuinely concurrent operations - multiple threads, processes, or HTTP requests - against the same resource at the same time, not sequential calls with a sleep.
  • Verify the final state is always correct across many runs: no lost updates, no duplicated effects, no bypassed checks (a balance that never goes negative, an inventory count that never oversells).
  • Re-run under load or with a race-detection tool where available, and re-scan with the security or concurrency analysis tool that reported the finding.

Common Vulnerable Patterns

// VULNERABLE - pseudo-code: check and write are two separate, unsynchronized steps
function withdraw(accountId, amount):
    balance = read_balance(accountId)       // read
    if balance < amount:
        error("insufficient funds")
    write_balance(accountId, balance - amount)  // write, based on a now-stale read

// Attack: two concurrent withdraw(accountId, 100) calls when balance = 100
// Both calls read balance = 100 and both pass the check before either writes.
// Result: balance ends at -100 instead of the second call being rejected.

Why this is vulnerable: the check is not wrong, it is stale. Between the read and the write, balance is a copy of a value another caller may already have changed, and nothing in the source marks that gap or says how many callers there are. Read once, alone, the function looks like correct code.

That is also why the defenses in front of it never see the attack. Both requests are well-formed, authenticated, within any rate limit, and individually legitimate; there is no payload to inspect and no anomaly in either one taken by itself. The whole of the attack is in the timing, so it can only be stopped where the state lives, by making the read and the write a single operation that the resource itself enforces.

Secure Patterns

// SECURE - pseudo-code: in-process lock covers the full read-modify-write sequence
acquire_lock(accountId)
try:
    balance = read_balance(accountId)
    if balance < amount:
        error("insufficient funds")
    write_balance(accountId, balance - amount)
finally:
    release_lock(accountId)

// SECURE - pseudo-code: the check and the update are one atomic database statement
affected = execute(
    "UPDATE accounts SET balance = balance - :amount
     WHERE id = :accountId AND balance >= :amount"
)
if affected == 0:
    error("insufficient funds or account not found")

Why this works: The lock-based version ensures no other operation can observe or modify the balance between the read and the write, so the check made under the lock is still true when the write happens. The database version goes further and removes the separate read entirely: the precondition (balance >= :amount) is evaluated by the database as part of the same atomic statement that performs the write, so there is no window in which another transaction can act on stale data. Either approach closes the gap that plain "read, decide, write" leaves open; an attacker cannot win a race against an operation that was never split into two observable steps.

Common Pitfalls

  • Locking only the read or only the write, not the sequence between them: Wrapping just read_balance() in a lock, releasing it, then acquiring a new lock for write_balance() - the check and the write are each individually synchronized, but another operation can still run in the gap between them, so the race window remains.
  • Using an in-process lock for state that is shared across multiple servers or worker processes: A mutex only protects the process that holds it; if the application runs more than one instance, another instance's process races right past an in-process lock because it never contends for the same lock object.
  • Relying on the database's default isolation level instead of an explicit lock or atomic statement: A plain SELECT followed later by an UPDATE can still lose an update under common isolation levels (such as read committed) unless the read is done with an explicit row lock or the update is made conditional on the value it read.
  • Treating client-side throttling or a disabled UI button as the fix: Debouncing a submit button or rate-limiting in JavaScript prevents an accidental double-click but does nothing against a scripted attacker sending two requests directly; concurrency has to be enforced server-side, at the resource itself.

Language-Specific Guidance

  • C# - lock, SemaphoreSlim for async code, Interlocked, System.Collections.Concurrent, and EF Core optimistic concurrency with [Timestamp]/RowVersion.
  • Go - sync.Mutex/sync.RWMutex, sync/atomic, sync.Map, the -race detector, and database-level atomicity for cross-process state.
  • Java - synchronized, java.util.concurrent.locks, java.util.concurrent.atomic, ConcurrentHashMap, and JPA/Hibernate @Version optimistic locking or SELECT ... FOR UPDATE.
  • JavaScript - race windows across await in async/Node.js code, promise-based mutexes, atomic database updates, and distributed locks for multi-instance deployments.
  • PHP - process-per-request races at shared resources: database transactions with SELECT ... FOR UPDATE, flock() for files, and atomic APCu operations.
  • Python - threading.Lock, asyncio.Lock for races across await points, multiprocessing synchronization, and database-level atomicity.

Additional Resources