Race Conditions and TOCTOU: When Timing Is the Vulnerability
A race condition occurs when the correctness of an operation depends on the timing of events the program does not control. In security terms the classic form is time-of-check to time-of-use — a program checks a condition, then acts on it, and something changes in the gap between the two.
The TOCTOU pattern
The structure is always the same. Check that a condition holds. Act on the assumption that it still holds. The window between them is where the attacker operates.
The filesystem case is the textbook one. A privileged program verifies that a file is owned by the user and is not a symbolic link, then opens it to write. Between the check and the open, the attacker replaces the file with a link to something privileged. The check passed on the real file; the write lands on the target.
The window may be microseconds, which sounds impractical and is not — an attacker repeats the attempt thousands of times, and can widen the window by loading the system or by using filesystem features that force the program to wait.
Where it appears beyond the filesystem
Financial and inventory logic. Read a balance, verify sufficient funds, deduct. Send the same request twenty times in parallel and every one may read the original balance before any writes land, so all twenty succeed. The double-spend and the discount code redeemed a hundred times are the same bug, and this is the version that shows up in web application testing — the business logic category under web application testing methodology that no scanner finds.
Authorization checks. Verify permission, then perform the action. If the permission is revoked in between, the action proceeds anyway.
Session and token handling. Validate a token, then use it. A token revoked in the window is still accepted.
Temporary files. Generate a predictable name, check it does not exist, create it. An attacker who creates it first — as a link — wins.
Signature verification. Verify a file's signature, then read the file again to use it. The two reads may see different content, which is a real pattern in update mechanisms and the reason verification and use must operate on the same bytes.
Why it is hard to find
The code looks correct. Each line does what it should, and the flaw is in the relationship between two statements rather than in either.
It is also non-deterministic. The bug manifests only when the timing aligns, so it passes tests, works in development, and appears occasionally in production under load — where it is dismissed as a transient glitch. Automated scanners find it rarely because reproducing it requires concurrent requests and observing an inconsistent outcome.
The fixes
Make the operation atomic. The general answer: eliminate the window rather than narrowing it. A database that performs the check and the update in a single statement, or within a transaction with appropriate isolation, has no gap. Narrowing the window is not a fix, because the attacker simply tries more often.
Use handle-based filesystem operations. Open the file first, then check properties of the open handle rather than of the path. The handle refers to the object that was opened, so replacing the path afterwards achieves nothing. Operating systems provide calls specifically for this, and using the path twice is the mistake.
Create files safely. Use the operating system's secure temporary file creation, which generates an unpredictable name and creates it atomically with the right permissions, rather than checking and then creating.
Lock explicitly where an atomic operation is not available, and prefer optimistic concurrency — a version number checked at write time, so a conflicting change is detected and the operation retried.
Enforce idempotency on operations that must happen once. A client-supplied unique key recorded on first use means a repeated request returns the original result rather than executing again — the same requirement as under webhook handling, and the practical defence against the parallel-request business logic case.
Rate limiting reduces how many attempts an attacker gets, and it is mitigation rather than repair.
Testing for it
Send the same request many times concurrently and check whether the outcome is consistent. Tooling for this exists in intercepting proxies, and it is a standard step in the manual work described under intercepting proxies. Focus on anything with a limit, a balance, a single-use token or a state transition.
In code review, look for a check and an action separated by any statement, especially where the subject is named by a path or an identifier that something else could change.
Exam angles
Define a race condition as behaviour dependent on uncontrolled timing, and TOCTOU as checking a condition and acting after it may have changed. Know the symlink replacement case and the parallel-request business logic case. Recognize that narrowing the window is not a fix and that atomic operations, handle-based file access, locking and idempotency keys are. Know that the flaw is non-deterministic, passes tests, and is found by concurrent request testing rather than by scanning.
Comments
Post a Comment