A race condition vulnerability exists when the correctness of an operation depends on the timing or ordering of events, and an attacker can influence that timing to make two operations that should be mutually exclusive overlap. The most common security-relevant form is TOCTOU — Time-Of-Check to Time-Of-Use — where a program checks a condition (funds available, file permissions, a rate limit) and then acts on it, but the state changes in the gap between the check and the use. It is classified as CWE-362, with TOCTOU as the specific CWE-367.
Race conditions span the whole stack. At the application layer they enable business-logic abuse: submitting the same coupon, withdrawal, or gift-card redemption many times in parallel so several requests all pass the "is it still valid?" check before any of them decrements the balance. At the kernel layer they are among the most severe bugs known — CVE-2016-5195, "Dirty COW," was a race condition in the Linux kernel's copy-on-write handling that let a local, unprivileged user gain root, and it was exploited in the wild for years across virtually every Linux distribution. Modern research has made web-layer races far easier to trigger: PortSwigger's 2023 "single-packet attack" technique can land dozens of requests within a sub-millisecond window, shrinking the race window attackers once considered impractical.
How Race Conditions Work
Most race conditions follow the check-then-act shape. Consider a withdrawal: the code reads the account balance, confirms it is at least the requested amount, then subtracts. That is safe only if nothing else touches the balance between the read and the write. Under concurrency, an attacker fires many identical withdrawal requests simultaneously. Each request reads the same starting balance, each passes the check because the subtraction has not happened yet, and each then subtracts — draining far more than the balance allowed. The account goes negative; the attacker walks away with money that never existed.
The window is the gap between check and act. Anything that widens it — network latency, slow storage, a lock held too briefly — makes exploitation easier, and attackers deliberately maximize concurrency to hit it. The same pattern recurs everywhere state is shared: incrementing a "one per customer" counter, promoting a user role, writing a file after checking its permissions, or reserving inventory. Egor Homakov famously demonstrated a race condition against Starbucks gift cards in 2015, transferring the same balance to multiple cards before the ledger caught up.
Vulnerable vs. Fixed
Read-check-write in application code without atomicity is the trap. The fix is to make the decision and the mutation a single atomic operation.
// VULNERABLE: check-then-act across two statements (TOCTOU)
async function withdraw(accountId, amount) {
const acct = await db.query("SELECT balance FROM accounts WHERE id=$1", [accountId]);
if (acct.balance >= amount) { // check
await db.query("UPDATE accounts SET balance = balance - $1 WHERE id=$2",
[amount, accountId]); // act — window between the two
}
}
-- FIXED: single atomic conditional update; the DB enforces the invariant
UPDATE accounts
SET balance = balance - :amount
WHERE id = :accountId
AND balance >= :amount; -- affects 0 rows if funds are insufficient
The atomic UPDATE makes the check part of the write: concurrent requests serialize on the row, and any that would overdraw simply update zero rows. Where an atomic statement is not possible, take a row lock (SELECT ... FOR UPDATE) inside a transaction, use a unique constraint to make duplicates impossible, or apply optimistic concurrency with a version column.
Prevention Checklist
- Make check-and-act atomic using conditional updates, database constraints, or compare-and-swap.
- Use row-level locking (
SELECT ... FOR UPDATE) or serializable transactions for multi-step invariants. - Add unique constraints so duplicate redemptions or double-inserts fail at the database.
- Apply idempotency keys to payment and mutation endpoints so replays collapse to one effect.
- Enforce rate limits and concurrency caps on sensitive actions to shrink the attacker's window.
- Avoid TOCTOU on the filesystem by operating on file handles/descriptors rather than re-resolving paths between check and use.
- Load-test for concurrency, firing parallel requests to confirm invariants hold under contention.
How Safeguard Detects Race Conditions
Race conditions only appear under concurrency, so Safeguard tests for them by exercising endpoints with tightly synchronized parallel requests rather than one at a time. The dynamic application security testing engine replays sensitive operations — redemptions, transfers, one-per-user actions — as bursts of simultaneous requests and checks whether an invariant (a balance, a counter, a uniqueness rule) was violated, surfacing logic races that sequential scanners never trigger.
Griffin AI explains each finding, maps it to CWE-362/CWE-367, and recommends the atomic-update or locking fix, which automated remediation can raise as a pull request. Because kernel- and library-level races are shipped in dependencies, software composition analysis also flags components with known race CVEs like Dirty COW. Running these checks in CI keeps concurrency testing in the pipeline; if you are comparing depth against a SAST-centric tool, see our Checkmarx comparison.
Frequently Asked Questions
What is the difference between a race condition and TOCTOU? A race condition is any timing-dependent flaw where overlapping operations produce an incorrect result. TOCTOU (Time-Of-Check to Time-Of-Use) is the most common security-relevant subtype: the specific case where state changes between when a program checks a condition and when it acts on that check.
Are race conditions only a problem in multithreaded code? No. They occur any time operations on shared state can overlap — across threads, across processes, and across independent HTTP requests hitting the same database row. Many of the most impactful web races involve no application threads at all, just concurrent requests racing on shared data.
Can input validation prevent race conditions? No. The requests in a race are individually valid; the flaw is in their timing and the non-atomic handling of shared state. Prevention comes from atomicity — conditional updates, locks, unique constraints, idempotency — not from filtering input.
Want to see whether your payment or one-per-user logic holds up under concurrent requests? Get started free or read the concurrency-testing guide in the Safeguard docs.