You checked the path, then used it — but the world changed in the gap between.
A TOCTOU race (time-of-check to time-of-use) happens when you inspect a file or path, decide it's safe, and then act on it as a separate step. Between those two steps an attacker — or just a second process — can swap what the path points to.
The classic shape is "check the file isn't a symlink, then open it." If an attacker replaces the file with a symlink to /etc/passwd in the window between the check and the open, your privileged write lands on the wrong target. The fix is to make check-and-use a single atomic operation on the same underlying object.
The vulnerable code looks safe: it validates, then acts. But access(), stat(), or "does this path exist?" all answer a question about a name, not a stable handle. By the time you call open(), the name may resolve to something else.
# Vulnerable: two lookups of the same name
if os.access(path, os.W_OK): # time of CHECK
with open(path, "w") as f: # time of USE — path may have changed
f.write(payload)
# Safer: open once, then check the handle you actually hold
fd = os.open(path, os.O_WRONLY | os.O_NOFOLLOW | os.O_CREAT, 0o600)
st = os.fstat(fd) # check the SAME object, by fd
if stat.S_ISREG(st.st_mode):
os.write(fd, payload)
The safe version never re-resolves the name. O_NOFOLLOW refuses symlinks at open time, and fstat inspects the exact descriptor you hold — there is no second lookup to race.
| Dimension | What to know |
|---|---|
| Race window | Microseconds — but attackers retry millions of times to land in it |
| Blast radius | Privileged writes/reads redirected to attacker-chosen files |
| Signal | Operations on a path resolve to an inode that differs from the one checked |
| Signal | Unexpected symlinks appearing in world-writable dirs like /tmp |
| Fix cost | Low — collapse check+use into one fd-based operation |
check(path) followed by act(path) is suspect. Hold an fd between them./tmp lets anyone plant a symlink. Use O_NOFOLLOW and per-user temp dirs.access(). It checks the real UID's permissions on a name, not on the handle you'll use — designed for a different question.fstat, never the path again.A backup tool runs as root and writes a log to /tmp/backup.log. It first checks the file is a regular file, then opens it to append. An attacker loops: delete /tmp/backup.log, then symlink it to /etc/shadow. If the symlink lands between the check and the open, root appends backup chatter into /etc/shadow — corrupting credentials. Replacing the check-then-open with a single os.open(path, O_WRONLY|O_NOFOLLOW) closes the window: the open itself refuses the symlink.
Why does using a file descriptor instead of re-opening the path defeat the race?
Where are TOCTOU races most likely?