Web Race Conditions: TOCTOU Testing and Defense
Learn how web race conditions and TOCTOU flaws break business logic, how to test concurrency safely, and which atomic controls actually fix them.
- AUTHOR
- Karol Rapacz / Pentester (OSCP, PNPT)
- PUBLISHED
- 4 May 2026
- READING TIME
- 15 min read
- TOPIC
- AppSec
A race condition in a web application occurs when the result of an operation depends on the order or timing of concurrent requests and the system does not protect the consistency of shared state. Two requests that are valid in isolation can enter a critical section together and produce a state the designer never intended: a coupon redeemed twice, a withdrawal beyond the available balance, multiple password resets, or an account used before verification finishes.
In one paragraph: testing a race condition is not about flooding an endpoint with hundreds of requests. First model the state machine and its business invariants. Then compare sequential execution with a controlled parallel attempt and prove the result using test-owned data. A proper fix must make the condition check and state change atomic; rate limiting alone rarely removes the root cause.
MITRE classifies this weakness as CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization. Its documented consequences include integrity violations, protection bypass, privilege gain, data exposure, and denial of service. In a web application, the vulnerable request can look perfectly correct on its own. The defect exists in the relationship between requests.
TOCTOU, race windows, and lost updates
The classic time-of-check to time-of-use, or TOCTOU, pattern separates a decision from its effect. An application checks whether a coupon is unused, calculates the discount, and only then records its use. If two processes finish the check before the first write, both approve the operation.
Request A: read used=false ── calculate ── write used=true
Request B: read used=false ── calculate ── write used=true
^ race window ^
The race window may last only milliseconds, but its duration does not determine severity. An attacker does not need to know the internal scheduler. They repeat a controlled set of parallel operations and inspect invariants: a balance must not fall below zero, a one-time token must not be accepted twice, and an order must not reach paid if its value changed after authorization.
Several useful classes should be separated during analysis:
| Class | Example | Broken invariant |
|---|---|---|
| limit overrun | repeated coupon or free-trial use | use count ≤ limit |
| lost update | two balance writes based on one version | no committed change disappears |
| multi-endpoint race | payment and cart update collide | paid value matches purchased goods |
| partial construction | account used during verification | no privileges before activation |
| double-spend | two withdrawals reserve one balance | reservations never exceed funds |
PortSwigger’s Smashing the state machine research showed why modern web races extend well beyond coupon duplication. Hidden substates, session caches, queues, asynchronous workers, and multiple endpoints changing the same object all matter.
Where race conditions hide
The best starting point is not a payload list. It is a threat model and a map of irreversible operations. Good candidates include:
- payments, refunds, withdrawals, reservations, and loyalty points;
- account activation, password reset, email change, and invitation acceptance;
- API quotas, MFA attempts, one-time codes, and anti-brute-force logic;
- creating uniquely named resources or assigning roles;
- file imports, report generation, and queued jobs;
- any
check → updatesequence, especially when split across services.
In a microservice architecture, the race may not exist inside one database. An order service reserves stock, a payment service confirms the charge, and a worker releases fulfilment. Every component can behave correctly in isolation while messages are duplicated, reordered, or delivered after a client timeout. An API penetration test therefore has to cover retry semantics, idempotency keys, and asynchronous process state.
A safe penetration-testing method
Only test within the agreed environment and with accounts owned by the assessment team. Concurrency can create hard-to-reverse effects: multiple payments, duplicate emails, repeated jobs, or inconsistent records. The rules of engagement should define the maximum request count, allowed objects, and cleanup procedure.
1. State the invariant
“This code can be used once” is a stronger hypothesis than “the endpoint may be vulnerable.” Record the initial state, expected outcome of one operation, and maximum allowed outcome of the batch. For a transfer, capture both balances, the transaction ID, and ledger total. For an invitation, capture its use count, resulting role, and expiration.
2. Establish a sequential baseline
Send the requests one after another. Record HTTP status, response body, timing, UI changes, and second-order effects such as email, audit events, history records, and queue work. If the application is unstable during sequential execution, a parallel result will be difficult to interpret.
3. Minimise timing differences
The Web Security Academy describes last-byte synchronisation for HTTP/1 and a single-packet technique for HTTP/2 to reduce network jitter. These techniques are diagnostic tools, not proof by themselves. Parallel delivery merely increases the chance that the backend processes operations inside the same race window.
4. Compare durable state, not just responses
Two 200 OK responses may be a false positive if only one transaction commits. Conversely, one response may report failure even though a worker performs both jobs. The evidence is durable state: two uses of one token, a negative balance, an extra resource, or a duplicate ledger entry.
5. Reduce the proof
After observing an anomaly, lower the number of requests, reproduce it, and find the smallest operation pair. Separate the effects of cache, sessions, connections, and regions. Do not escalate into real damage. A controlled violation of the invariant is sufficient evidence.
A professional web application penetration test also records reproduction frequency. A race that triggers twice in fifty attempts can still be critical, but the engineering team needs enough information to distinguish the defect from infrastructure noise.
Why common fixes fail
Rate limiting
A request limit reduces attempt volume, but two requests can still enter one window. The limiter may be distributed across nodes, based on a delayed counter, or bypassable through several sessions. It is an abuse-control layer, not a replacement for atomic state change.
A processing=true flag
If code first reads the flag and then sets it, the flag has its own race. It must be acquired conditionally through an atomic operation, such as a conditional update or a lock backed by the actual state store.
An in-process lock
A mutex in one instance does not protect against another pod, worker, or region. A scaled architecture must identify where the shared state really lives and which layer guarantees serialisation.
A longer transaction with the wrong isolation
A transaction does not automatically eliminate a race. The isolation level and exact queries determine whether two processes observe the same state. PostgreSQL documents the behaviour of its isolation levels and the serialization errors an application may need to retry in Transaction Isolation.
Controls that address the cause
Prefer invariants enforced as close to the data as possible. A unique constraint can guarantee a single consumption record for a token. A conditional UPDATE ... WHERE used = false checks the number of changed rows without a separate read. Balances may require row locks, version checks, or SERIALIZABLE isolation with deliberate retry behaviour.
An idempotency key binds a logical operation to a unique identifier. The server persists the result of the first execution and returns it for a retry instead of applying the effect twice. The key must be scoped to the principal, request parameters, and retention period. Accepting an arbitrary header without durable deduplication does not provide idempotency.
Distributed workflows need explicit state machines. Allowed transitions such as created → authorized → captured should be conditional and monotonic. Queue consumers must tolerate duplicate delivery. Inbox and outbox patterns can connect a database change to message publication, but they still require a stable operation key and correct retry handling.
| Problem | Preferred control | Regression test |
|---|---|---|
| one-time token | unique constraint + atomic consume | concurrent use of one token |
| resource quota | conditional update in one transaction | N+1 operations at limit N |
| duplicate charge | idempotency key + ledger | retry after client timeout |
| state transition | compare-and-swap / versioning | two conflicting transitions |
| repeated message | idempotent consumer | deliver the same message twice |
The fix needs a concurrent CI test. A sequential unit test may never enter the race window. Integration tests can use synchronisation barriers, controlled pauses between read and write, and assertions against final database state.
Reporting a race condition clearly
The report should state the broken invariant, initial state, two minimal operations, attempt count, durable effect, and test boundaries. Include request and record identifiers while removing session tokens and personal data. Rather than writing “missing lock,” explain where the decision and write were separated and which control should provide atomicity.
Show business impact through a realistic but safe scenario. Repeated discount redemption means financial loss. A password-reset race can lead to account takeover. A concurrent role change can violate authorization. Severity should reflect repeatability, automation potential, and whether monitoring can detect the outcome.
OWASP ASVS 5.0 can turn a one-off fix into an SDLC requirement, while the comparison of SAST, DAST, and IAST explains why no single tool finds every race. Static analysis may flag a check-then-act sequence, but only a test against the real state store confirms the system behaviour. Related state and inheritance defects, such as JavaScript prototype pollution, likewise require testing application semantics rather than matching one payload.
Engineering checklist
- Does every financial or one-time operation have a documented invariant?
- Are the condition check and state change atomic in the same data layer?
- Does the database enforce uniqueness instead of application code alone?
- Are retries by clients, gateways, and queues safe?
- Is each idempotency key durable, parameter-bound, and collision-resistant?
- Does the state machine reject backward and conflicting transitions?
- Do regression tests execute operations concurrently?
- Does monitoring detect negative balances, repeated consumption, and duplicate effects?
A race condition is a logic and architecture flaw, not a network-speed problem. If a critical operation depends on the assumption that “the second request will not arrive in time,” the assumption is already a security defect. Need your payment, account, or API workflows assessed? Book a web and API penetration test — we will test concurrency using controlled data and provide a reproducible regression case.


