A race condition turns a correct check into a wrong decision because something changed between the moment the code checked and the moment it acted. In web applications that means a voucher redeemed twice, a balance overdrawn or a limit exceeded when several requests arrive together; in operating systems and CI/CD it means a file swapped or a token used inside a short window. This guide explains the weakness family (CWE-362 and its children, including TOCTOU), where it appears, how to detect it and how to design it out with atomic operations, locking, isolation levels and idempotency. It is for developers, architects and application security teams; it is defensive and gives no exploitation procedure. For where these weaknesses rank among the most dangerous software flaws generally, see the CWE Top 25 Guide; this page goes deeper on concurrency design only.
Key takeaways
- Check and act must be one step. OWASP ASVS 5.0 V15.4.2 requires that checks on a resource's state and the actions that depend on them run as a single atomic operation to prevent TOCTOU.
- Read-then-write in application code is the usual bug. Reading a balance, deciding in code and then writing back leaves a window; a conditional update or a row lock closes it.
- Know your database default. PostgreSQL defaults to Read Committed, which permits nonrepeatable reads and serialisation anomalies; Serializable prevents them but requires retrying transactions that fail with SQLSTATE 40001.
- Constraints beat code. A unique constraint on a redemption record holds no matter how many requests race, and is the cheapest durable fix for single-use actions.
- Short windows are still windows. Unit 42's ArtiPACKED research showed a token valid only until a CI job ended could still be used within that time; shrinking a window reduces risk but does not remove it.
How the attack works
MITRE defines CWE-362 Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition') as a code sequence that needs temporary exclusive access to a shared resource, with a timing window in which another concurrent sequence can modify it. MITRE describes two properties the sequence should have: exclusivity (no other sequence can change the resource until it finishes) and atomicity (the sequence behaves as one indivisible unit). A race condition is the loss of either.
CWE-367 Time-of-check Time-of-use (TOCTOU) is the most common form: the product checks a resource's state before using it, and the state changes between check and use, invalidating the check. In a web application the "resource" is usually a database row (a balance, a stock count, a coupon's used flag); in an operating system it is often a file path or permission.
| CWE | Name | Typical setting |
|---|---|---|
| CWE-362 | Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition') | Parent class for all of the below |
| CWE-367 | Time-of-check Time-of-use (TOCTOU) Race Condition | Web limit checks, file access checks |
| CWE-364 | Signal Handler Race Condition | Native code handling asynchronous signals |
| CWE-366 | Race Condition within a Thread | Shared in-memory state in multi-threaded services |
| CWE-368 | Context Switching Race Condition | Operations that switch privilege or security context |
| CWE-421 | Race Condition During Access to Alternate Channel | Side channels opened for a privileged operation |
| CWE-689 | Permission Race Condition During Resource Copy | Files copied before permissions are applied |
| CWE-1223 | Race Condition for Write-Once Attributes | Hardware and firmware registers |
MITRE CAPEC describes the attacker view as CAPEC-26 Leveraging Race Conditions and CAPEC-29 Leveraging Time-of-Check and Time-of-Use (TOCTOU) Race Conditions. Defenders can use those entries to frame threat models; this guide stays on the design side.
Where it shows up
- Limits and single-use actions: coupon and gift card redemption, referral bonuses, one-vote-per-user, free-trial activation, withdrawal limits. The code checks "not yet used" or "within limit" and then records the use in a separate statement.
- Checkout and inventory: the last item in stock sold to two buyers, or a seat booked twice. ASVS V2.3.4 asks for business-level locking so limited quantity resources such as theatre seats or delivery slots cannot be double booked.
- Balances and ledgers: transfers that read a balance, subtract in application code and write the result, allowing concurrent transfers to overdraw.
- Authentication and session state: one-time codes or recovery tokens accepted twice if the "consumed" flag is set after validation; session upgrade and logout racing each other.
- Files and temporary resources: a program checks permissions on a path and then opens it, letting the path be replaced in between (the classic CWE-367 case in native code).
- Multi-threaded services: caches and shared objects mutated without synchronisation. ASVS V15.4.1 requires thread-safe types and locks or semaphores for shared objects.
- CI/CD and short-lived credentials: tokens valid only while a job runs can still be used if they leak during the run. See the GitHub and GitHub Actions Security Guide.
- Distributed systems: two service instances, or a retry and its original, both processing the same message because deduplication happens after the side effect.
Impact
| Impact | Example | Severity driver |
|---|---|---|
| Limit bypass and financial loss | A single-use credit applied several times | Value per action and how easily concurrency is achieved |
| Data integrity | Lost updates, negative stock, ledger out of balance | Whether reconciliation detects it |
| Privilege escalation | File replaced between permission check and use by a privileged process | Privilege of the vulnerable process (CWE-362 mitigations include running with least privilege) |
| Credential misuse | A leaked token used before it expires | Token scope and lifetime |
| Availability | Deadlocks or thread starvation from poor locking (ASVS V15.4.3, V15.4.4) | Lock scope and retry behaviour |
Detection
Before release
- Code review patterns: flag any "select, decide in code, then update" sequence on shared state, any check-then-open on file paths, and any shared mutable object without synchronisation. See the Secure Code Review Guide.
- Concurrency tests: for every single-use or limited action, add an automated test that fires many identical requests in parallel against a test environment and asserts the invariant still holds (one redemption, no negative balance). This is a functional test of your own system, not an attack tool.
- Static and dynamic tooling: thread sanitisers and static analysers find some in-process data races; MITRE notes static analysis can find some instances, but business-level races across requests generally need targeted tests.
- Penetration testing: include concurrency in scope for transactional flows. See the Web Application Security Testing Guide.
In production
- Invariant monitors: alert on negative balances or stock, a voucher with more than one redemption, or more orders than inventory.
- Burst detection: identical requests from one session or key arriving within milliseconds on sensitive endpoints.
- Database signals: rates of serialisation failures (SQLSTATE 40001), deadlocks and lock waits; a sudden rise may reveal contention on a flow you did not know was shared.
- Idempotency conflicts: count 409 responses for in-flight duplicate keys; spikes indicate aggressive retries or abuse.
Metrics:
- Share of single-use and limited endpoints protected by a constraint, conditional update or lock (target 100%).
- Share of those endpoints with a parallel-request test in CI.
- Invariant violations detected by reconciliation per month, trending to zero.
- Serialisation failure and deadlock rates, with retries succeeding within the configured limit.
- Race findings per penetration test or bug bounty period.
Prevention and hardening
Design patterns
| Pattern | How it closes the window | Use when | Trade-off |
|---|---|---|---|
| Database constraint (unique, check) | The second conflicting write fails at commit | Single-use claims, one record per key, non-negative values | Must handle the violation error cleanly |
| Conditional atomic update | Check and change in one statement, for example update only where balance is sufficient, then confirm one row changed | Counters, balances, stock decrements | Logic must fit in one statement |
| Pessimistic row lock (SELECT ... FOR UPDATE) | Other writers block until the transaction ends | Multi-step logic on a few hot rows | Contention and deadlock risk; keep transactions short |
| Optimistic concurrency (version column) | Write succeeds only if the version is unchanged | Low contention, user edits | Caller must retry on conflict |
| Serializable isolation | Database aborts transactions that could not have run serially | Complex invariants across many rows | Must retry whole transaction on 40001; more aborts under load |
| Idempotency key | Duplicate requests return the stored result instead of re-executing | Payments, order creation, any retried POST | Key store must itself be atomic |
| Single-writer queue or partition | All changes to one entity are processed serially | High-throughput ledgers, event-driven systems | Architecture change; added latency |
Database specifics
- Isolation level: PostgreSQL's default Read Committed takes a new snapshot per statement, so two reads in one transaction can differ. Repeatable Read fixes the snapshot but can still allow serialisation anomalies. Serializable guarantees committed transactions have the same effect as running one at a time, using non-blocking predicate locks.
- Retry correctly: PostgreSQL advises that applications at Repeatable Read or Serializable must retry on serialisation failure, rerunning the whole transaction rather than the failed statement, with a limit and backoff.
- Row locks: FOR UPDATE blocks other UPDATE, DELETE and SELECT FOR ... on the same rows until the transaction ends; FOR SHARE allows other readers that lock in share mode but blocks writers.
- Advisory locks: application-defined locks that the server does not enforce; prefer transaction-level advisory locks, which release automatically at transaction end, over session-level ones, which survive rollback.
- Deadlocks: PostgreSQL detects and aborts one transaction; prevent them by acquiring locks on multiple objects in a consistent order and avoid holding transactions open while waiting for user input. See the Database Security Guide.
Code and platform
- Lock before checking: MITRE's CWE-367 mitigations include acquiring the lock before the check so the resource checked is the resource used, and rechecking after use.
- Use atomic primitives: CWE-362 notes that statements such as x++ are not atomic at the instruction level; use atomic types, mutexes around only the critical section, and thread-safe libraries.
- Files: operate on file handles rather than repeatedly on paths, create temporary files with exclusive-create semantics, and run with the least privilege needed.
- Consume before acting: mark one-time tokens as used in the same atomic step that validates them.
- Distributed locks with care: a lock in a cache cluster can expire while work continues; pair it with a fencing check (a version or constraint) at the database.
- Short-lived credentials: scope CI tokens to the minimum permissions and keep secrets out of build artefacts, so a leak inside the validity window has little value. See the CI/CD Security Guide.
Rollout
- Find shared state: list endpoints that change balances, counters, stock, single-use items or tokens.
- Write invariants: state each rule in a testable form ("a voucher has at most one redemption").
- Add tests first: write parallel-request tests that currently fail where a race exists; they prove the fix later.
- Fix with the lightest durable control: constraint, then conditional update, then lock or isolation change. Deploy behind a feature flag and pilot.
- Watch contention: monitor lock waits, deadlocks and 40001 rates during the pilot; the flag is the rollback.
- Add reconciliation: scheduled invariant checks catch anything the design missed. Fold the patterns into the Secure Software Development Lifecycle (SSDLC) Guide standards so new services start safe.
Decision guidance
- Small team, single database: rely on unique and check constraints plus conditional updates; they need no new infrastructure.
- Complex invariants across tables: use Serializable for those transactions only, with a tested retry wrapper.
- Hot rows under heavy load: prefer conditional updates or a single-writer queue over long pessimistic locks.
- Public APIs with retries: require idempotency keys; the IETF Idempotency-Key header draft (work in progress) suggests 409 Conflict while the original request is in progress and 422 when a key is reused with a different payload. See the API Security Guide.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| OWASP ASVS 5.0 | V15.4.1 to V15.4.4 Safe Concurrency; V2.3.3; V2.3.4 | Thread safety, atomic check-and-act, consistent locking, fairness, transactions, business locking |
| MITRE CWE | CWE-362, CWE-367, CWE-364, CWE-366, CWE-368 | Root cause mapping |
| MITRE CAPEC | CAPEC-26, CAPEC-29 | Attack patterns for threat models |
| NIST SP 800-53 Rev. 5 | SA-8, SA-11, SC-24, SI-4 | Engineering principles, developer testing, fail in known state, monitoring |
| NIST CSF 2.0 | PR.PS-06, ID.RA-01, DE.CM-09 | Secure development, vulnerability identification, monitoring |
Lessons from incidents
- A short validity window was enough: in ArtiPACKED 2024, Unit 42 found GITHUB_TOKEN values persisted in public build artefacts. The token expired when the job finished, but artefacts could be downloaded while the workflow was still running, so where steps followed the upload there was a window to use it. The lesson: expiry is not a substitute for keeping the secret out of the artefact.
- Checks in code, writes elsewhere: validating a limit in the application and recording the use in a later statement is the single most common web race pattern.
- Raising isolation without retries: switching to Serializable and then surfacing 40001 errors to users or dropping the transaction trades a security bug for an availability bug.
- Distributed locks without fencing: a cache lock that expires mid-operation lets a second worker proceed; the database must still reject the conflicting write.
- Testing serially: functional suites send one request at a time and never see the race.
Practitioner checklist
- Inventory endpoints that change balances, counters, stock, single-use items or tokens.
- Express each invariant as a database constraint wherever possible.
- Replace read-decide-write sequences with conditional atomic updates and check the affected row count.
- Use row locks or Serializable isolation only where needed, with whole-transaction retries on SQLSTATE 40001 and deadlocks.
- Acquire locks in a consistent order and keep transactions short.
- Require idempotency keys on payment and creation endpoints and store results atomically.
- Mark one-time tokens consumed in the same step that validates them.
- Add parallel-request tests for every limited action to CI.
- Monitor invariant violations, lock waits, deadlocks and serialisation failures.
- Keep secrets out of build artefacts and scope short-lived tokens minimally.
Standards and references
- MITRE CWE: CWE-362 Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')
- MITRE CWE: CWE-367 Time-of-check Time-of-use (TOCTOU) Race Condition
- OWASP: Application Security Verification Standard 5.0
- MITRE CAPEC: Leveraging Race Conditions
- PostgreSQL: Transaction Isolation
- PostgreSQL: Explicit Locking
- IETF: The Idempotency-Key HTTP Header Field (Internet-Draft)
- Palo Alto Networks Unit 42: ArtiPACKED, GitHub Actions Artifacts Leaking Tokens
Related NHI Mgmt Group resources: CWE Top 25 Guide · API Security Guide · Secure Software Development Lifecycle (SSDLC) Guide · Database Security Guide