Race conditions happen when application security depends on timing.
The primitive is state changing between two operations. The application checks that something is safe, but it uses that thing later after the attacker has changed it.
The classic security race is TOCTOU:
Time Of Check
Time Of Use
The application checks one state, then uses a different state.
Core Idea
A race condition needs at least two actors:
- The application
- The attacker
And at least one shared object:
- File path
- Temporary file
- Symlink
- Database row
- Lock file
- Queue message
- Session state
- Uploaded file
- Permission record
The vulnerable pattern is:
check condition
gap
use resource
If an attacker can change the resource during the gap, the original check may no longer mean anything.
TOCTOU
TOCTOU means the application checks a resource, then later uses that resource.
Vulnerable filesystem pattern:
if path.is_file():
data = path.read_text()
That looks reasonable, but the path can change between is_file() and read_text().
An attacker may replace the checked file with:
- Another file
- A symbolic link
- A hard link
- A different directory entry
- A file with different permissions
The issue is not only the function names. The issue is that the check and the use are separate operations.
A realistic gap often appears after validation. The program may load a file, verify its headers, parse metadata, update an index, write logs, or queue a job, then reopen the same path later for the final operation. If the final operation trusts the path instead of the object that was already validated, the race is still present.
Common Race Targets
Filesystem races:
- Check file, then open file
- Check owner, then write file
- Check extension, then process file
- Check upload, then move upload
- Delete predictable file, then recreate it
- Create temporary file with predictable name
- Follow symlink in a writable directory
Application logic races:
- Check balance, then withdraw
- Check coupon, then redeem
- Check stock, then purchase
- Check role, then perform action
- Check password reset token, then consume it
- Check invite code, then create account
Concurrency races:
- Two workers process the same job
- Two requests update the same record
- One request changes state while another assumes it is stable
- A cleanup task deletes or rewrites a file while another task reads it
Why Filesystem Races Matter Locally
Local applications often interact with paths in writable directories.
Interesting locations:
/tmp
/var/tmp
/dev/shm
application cache directories
application spool directories
upload staging directories
backup directories
When a privileged or trusted process uses attacker-controlled paths, race conditions can become serious.
Typical impact:
- Read a file that should not be readable through the app
- Overwrite a file that should not be writable through the app
- Redirect logs or reports
- Replace scanned content after validation
- Abuse symlink handling
- Escalate privileges when a privileged process writes through the raced path
This connects directly to the Linux Symlink Attacks lesson. Symlinks are often the object used to win a TOCTOU race.
Enumeration
Look for code patterns:
exists() then open()
is_file() then open()
access() then open()
stat() then open()
realpath() then later open(original_path)
delete then recreate
check extension then process path
scan upload then serve path
Look for shell patterns:
test -f "$path" && cat "$path"
[ -e "$file" ] && echo data > "$file"
rm -f /tmp/name
sleep 1
cat input > /tmp/name
Look for behavior:
- Predictable filenames
- Writable parent directory
- Delays between validation and use
- Cleanup jobs
- Temporary upload paths
- Services running with more privilege than the user
- Repeated file operations on the same path
- Symlink-following behavior
Useful tracing tools:
strace -f -e trace=file ./program
inotifywait -m /tmp
pspy64
strace shows file syscalls. inotifywait shows filesystem events. pspy helps spot scripts and scheduled jobs on Linux targets.
Exploit Workflow
Use this workflow:
- Identify the check.
- Identify the use.
- Identify the shared resource.
- Confirm the attacker can modify that resource.
- Measure or widen the race window.
- Automate the swap.
- Repeat until the use sees the attacker-controlled state.
- Prove impact with a harmless target first.
The race window may be tiny. Exploits usually repeat the action many times.
Race windows can be widened by:
- Large files
- Slow filesystems
- Network filesystems
- Compression or scanning work
- Debug sleeps in a lab
- High CPU or IO load
- Many repeated attempts
Remediation
Patch race conditions by making the check and use atomic, or by checking the object actually used.
Filesystem guidance:
- Avoid predictable temporary filenames.
- Use safe temporary file APIs such as
mkstemp. - Open files once, then validate the opened file descriptor.
- Do not validate a path and later reopen the path.
- Avoid following symlinks for sensitive operations.
- Use
O_NOFOLLOWwhere appropriate. - Use restrictive directory permissions.
- Avoid privileged writes into user-writable directories.
- Use file locks only when they protect the real shared resource.
Application logic guidance:
- Use database transactions.
- Use row locks or optimistic concurrency checks.
- Enforce unique constraints.
- Make token consumption atomic.
- Move state transitions into one transaction.
- Treat retries and duplicate requests as normal.
Safe filesystem pattern:
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
st = os.fstat(fd)
if not stat.S_ISREG(st.st_mode):
raise ValueError("not a regular file")
with os.fdopen(fd, "r") as f:
data = f.read()
The application validates the opened file, not a path that may change later.
Key Takeaway
Race conditions are not about typing faster than the program.
They are about finding a gap where the application assumes state is stable, then changing that state before the application uses it.
The safe design removes the gap.