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 thanstat()followed byopen()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()withO_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, whilestat(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 withopenat2()andRESOLVE_BENEATHon 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()orfcntl()withF_SETLKserialise 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 byopen()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()beforeopen(),O_NOFOLLOW,O_CREAT|O_EXCL,openat2()withRESOLVE_BENEATH - Java - authorization re-read inside the transaction, JPA
@Versionoptimistic locking,CREATE_NEWandSecureDirectoryStream - Python - why the GIL does not help, when
threading.Lockis scoped to one worker,os.openwithO_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,
CompareAndSwapretry loops,go test -race