Skip to content

CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition

Overview

A TOCTOU race condition appears when a security check and the use of the checked resource are separate steps. In the interval between them an attacker can change the state the check was based on - replacing a file with a symlink, changing permissions, altering a balance - so the use goes ahead on a decision that is no longer true.

Relationship to Other CWEs

CWE-367 is the check-then-use shape: a value or a resource is examined, and a later step acts on the result of that examination. The entries below place it against the two CWEs that cover the rest of the ground.

  • CWE-367 (this page) - the security check and the use of the checked resource are separate steps, so the state the check was based on can change in between.
  • CWE-362 (Race Condition) - the general weakness. Every TOCTOU flaw is a race condition, but CWE-362 also covers races with no check in them at all - two concurrent increments of the same counter, or two writers interleaving on the same file. Use it when the finding does not decompose into a check and a use.
  • CWE-366 (Race Condition within a Thread) - scoped to state shared between threads of a single process. That scope decides the fix: an in-process lock is sufficient there, and is not sufficient here as soon as the state is shared across workers, replicas or the filesystem.

Risk

High: An attacker who wins the race gets the outcome the check was meant to prevent. That covers privilege escalation, where the permissions read by the check are changed before the use; symlink attacks, where the checked file is replaced with a symlink to a sensitive one; double-spending, where a balance is checked and the deduction lands after a second transaction; and authentication bypass.

Remediation Steps

Core Principle: Make the check and the use a single atomic operation - using locking, file descriptors or a transaction - and where that is not possible, re-check at the time of use.

Locate the TOCTOU race condition

  • Start from the finding: the file, the line, and the code where the check is separated from the use
  • Name the two halves: the check (a permission, an existence or file test, a balance, an authentication decision) and the operation that acts on its result
  • Identify the race window: the interval between check and use in which an attacker can change the state

Make check-and-use atomic (Primary Defense)

  • Use file descriptors instead of paths: open the file and call fstat(), rather than stat() followed by open() on the path
  • Acquire the lock before the check, hold it through the use, and release it afterwards
  • Use compare-and-swap where the check and the update apply to a single value
  • Prefer an API that does both in one call, such as open() with O_CREAT|O_EXCL

File system TOCTOU prevention

  • Wrong pattern: if (file.exists()) { fd = open(file); } - the attacker can replace the file between the check and the open
  • Right pattern: fd = open(file, O_CREAT|O_EXCL); - atomic check and create
  • Verify after opening, not before: once the file is open, fstat(fd) reports on the file that was actually opened, while stat(path) resolves the name again and can answer about a different one
  • Pass file descriptors, not paths: a path handed on to another function is re-resolved there, so the check the caller made does not travel with it
  • Use O_NOFOLLOW to refuse a symlinked final component. It constrains only the last component, so where a parent directory is attacker-writable, work relative to an open directory with openat(), or with openat2() and RESOLVE_BENEATH on Linux

Use exclusive locks

  • Acquire the lock before checking permission, balance or existence
  • Hold it through the use; do not release it between the check and the use, and where the operation spans several statements, hold it for the whole transaction
  • For financial operations, use a database transaction - BEGIN TRANSACTION ... COMMIT - with row-level locks
  • Advisory file locks coordinate access between processes: flock() or fcntl() with F_SETLK serialise access to a shared file across cooperating processes. They are advisory, so they only work if every writer takes the lock - a process that ignores it still writes freely, which makes them a coordination mechanism rather than an access control.

Test the TOCTOU fix

  • Verify that check-and-use is atomic: a single operation, or one lock covering both
  • Try to change the state concurrently between check and use; this should be impossible
  • Try to replace the file with a symlink between the operations; with O_NOFOLLOW this should fail
  • Try to deduct the balance twice concurrently; with the locking in place the second attempt should fail
  • Re-scan with the security scanner to confirm the issue is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
if resource_is_acceptable(name):   // TIME OF CHECK
    ...                            // race window: any yield, syscall, or await
    use_resource(name)             // TIME OF USE - resolved again, may differ

Why this is vulnerable: the check is correct and the use is correct; the defect is that they are two operations against a name rather than one operation against a thing. Three families cover almost all findings:

  • Filesystem. A path is re-resolved on every call, so access()/stat() followed by open() can act on a different file - typically a symlink the attacker substituted. The window is the interval between two syscalls, not anything visible in the source.
  • Authorization. A permission read from a session, cache, or earlier query, then acted on later. A concurrent revocation lands in between and the operation proceeds on a decision that is no longer true.
  • Value invariants. Balance, stock, or quota read, tested, then written back. Two callers read the same value, both pass, both write - the invariant ("never negative", "never oversold") breaks without either code path being individually wrong.

Secure Patterns

// SECURE - pseudo-code: one operation that both checks and acts
handle = open_exclusive(name)      // kernel checks existence and creates atomically
verify(handle)                     // checks refer to this handle, not to the name

// or: let the store evaluate the condition while it holds the lock
UPDATE accounts SET balance = balance - :amount
 WHERE id = :id AND balance >= :amount

Why this works: Both forms remove the interval rather than shortening it. Holding a handle means later checks refer to the object that was opened, so renaming or relinking the path afterwards changes nothing. Expressing the condition inside the write means the store evaluates it while holding the lock it needs to write, so no interleaving exists in which two callers both pass.

Re-checking at the point of use is the weaker sibling: it narrows the window without closing it. That is an acceptable answer for session revocation, where losing a narrow race is tolerable, and not for a balance or a unique-slot allocation, where the invariant has to hold exactly.

Database Transactions with SELECT FOR UPDATE

-- SECURE - the row is locked for the whole check-then-update
BEGIN;

SELECT quantity
  FROM inventory
 WHERE product_id = 123
   FOR UPDATE;              -- other transactions wait here

-- application checks quantity, then:
UPDATE inventory
   SET quantity = quantity - 10
 WHERE product_id = 123;

COMMIT;

Why this works: FOR UPDATE takes the row lock during the read rather than at the write, so the value cannot change between the two statements - a concurrent transaction blocks until this one commits. Use it when the decision needs more than arithmetic (writing to other tables, calling out to a service). Where the condition can be written as a predicate, the single conditional UPDATE above is cheaper: it needs no explicit transaction and holds the lock for a shorter time.

Isolation level matters, and the default is not the same everywhere: PostgreSQL, SQL Server and Oracle default to READ COMMITTED, MySQL's InnoDB to REPEATABLE READ. Neither default prevents two transactions from reading the same value and both writing it back - under REPEATABLE READ the plain read can even be older than the committed row, because it comes from the transaction's snapshot while the UPDATE sees the current one. It is the locking, or the condition being evaluated at write time, that provides the guarantee.

Language-Specific Guidance

  • C - access()/stat() before open(), O_NOFOLLOW, O_CREAT|O_EXCL, openat2() with RESOLVE_BENEATH
  • Java - authorization re-read inside the transaction, JPA @Version optimistic locking, CREATE_NEW and SecureDirectoryStream
  • Python - why the GIL does not help, when threading.Lock is scoped to one worker, os.open with O_EXCL
  • JavaScript - why a single-threaded event loop still races across await, re-validation and idempotency keys
  • Go - why two atomic operations are not an atomic section, CompareAndSwap retry loops, go test -race

Additional Resources