Race Conditions and TOCTTOU: When Timing Becomes a Vulnerability
Most vulnerability classes involve accepting bad input. Race conditions are different. The input is fine, the logic is correct, and the check passes legitimately — the flaw is that the world changed between the check and the action that relied on it.
Security+ tests this under the name TOCTTOU: time of check to time of use.
The Shape of the Bug
Code does two things in sequence:
- Verify a condition — the file is safe, the balance is sufficient, the user is authorized.
- Act on that condition — open the file, deduct the amount, grant the resource.
Between those two steps there is a window. If an attacker can change the underlying state inside that window, the action proceeds on a verification that is no longer true. The program never did anything wrong; it simply trusted a fact that expired.
The classic file example: a privileged program checks that a path points to a file the calling user owns, then opens it. An attacker replaces the path with a symbolic link to a system file in between. The check passed on the harmless file; the open happens on the sensitive one.
Where It Shows Up
File system operations. Any check-then-use pattern on a path in a directory the attacker can write to. Temporary directories are the recurring offender, and it is one of the reasons Linux permissions on shared temporary space matter more than they look like they should.
Financial and inventory logic. Check balance, then deduct. Fire many concurrent requests and several may pass the check against the same starting balance before any of them writes. This is how gift card and refund abuse works in practice.
Authorization caching. Permission is verified at the start of a long operation and not rechecked. Access revoked mid-operation has no effect.
Setup and installation scripts. A file created with permissive permissions and tightened a moment later is readable during the gap. Same for secrets written to disk before being locked down.
Double-fetch in kernel and driver code. A value is read from user space, validated, then read again for use. The second read returns something different.
Fixes That Work
The general principle: make the check and the action a single indivisible operation, or make the check unnecessary.
- Use atomic operations. Open the file and then inspect the handle, rather than inspecting the path and then opening it. Operate on file descriptors, which cannot be swapped out from under you, rather than on names, which can.
- Use the operating system's atomic primitives. Exclusive-create flags, atomic rename, file locking — these exist precisely for this problem.
- Use database transactions and row locking. For the balance case, a transaction with appropriate isolation, or an atomic conditional update, removes the window entirely. Application-level checking before a write is the thing to avoid.
- Idempotency keys. For request-level races, a client-supplied key that the server records means duplicate submissions collapse into one operation.
- Drop privileges early. Much of the file-based exploitation depends on the program running with more authority than the requesting user. Mandatory access control such as SELinux constrains what a compromised privileged process can reach even when the race succeeds.
Retrying after a failure is safer than checking before acting. Attempt the operation, handle the error, and you never depend on a fact that can go stale.
Why These Are Hard to Find
Race conditions are timing-dependent and frequently non-deterministic, so they do not reproduce reliably and may never appear in testing. They surface under load, which means production.
Static analysis catches some known patterns — the classic check-then-open sequence is recognizable. Dynamic testing struggles, because fuzzing varies input rather than timing. Finding them deliberately usually means concurrency-focused testing: sending many identical requests simultaneously and looking for state that should have been impossible.
That last technique is worth knowing for PenTest+. Fire fifty concurrent redemption requests for a single-use coupon; if more than one succeeds, there is a race in the redemption path.
Exam Focus
Know what TOCTTOU stands for and that the vulnerability lives in the window between check and use. Recognize the symlink-swap scenario. Recognize concurrent requests each passing the same balance check as a race rather than as an authorization flaw. Know that the fix is atomicity — locking, transactions, atomic system calls — and not simply making the window smaller, since a shorter window is still a window.
Race conditions appear on the OWASP radar as a business logic flaw, which is the category most likely to survive both automated scanning and a checklist-driven review.
Comments
Post a Comment