This weakness occurs when a program checks whether a file is safe to access for example, by verifying it's not a symbolic link pointing elsewhere, but an…
This weakness occurs when a program checks whether a file is safe to access (for example, by verifying it's not a symbolic link pointing elsewhere), but an attacker can swap the file between the check and the actual use. The program trusts its earlier verification, unaware the target has changed, and proceeds to read or write a file the attacker controls. This is a classic "time-of-check to time-of-use" (TOCTOU) vulnerability specific to symlink manipulation.
02How It Happens
The vulnerability arises when code performs a security check on a file path, then uses that path later in a separate operation, without holding a lock or guarantee that the file hasn't changed. An attacker with filesystem access can replace a regular file with a symbolic link (or vice versa) in the microseconds or milliseconds between the check and the use. The program's check may have confirmed "this is a safe regular file in the temp directory," but by the time the program opens it, a symlink now points to /etc/passwd or another sensitive location. The program, believing it validated the path, proceeds without re-checking.
This is especially dangerous in privileged processes (daemons, setuid binaries, or services running as root) that create or modify files in world-writable directories like /tmp. An unprivileged attacker can exploit the race window to trick the privileged process into reading or overwriting files outside the intended scope.
03Real-World Impact
Successful exploitation can lead to unauthorized file read (information disclosure of sensitive system files or application secrets), unauthorized file write (overwriting critical configuration or application files), or privilege escalation if a privileged process is tricked into modifying files it shouldn't. In multi-user systems, this is a common vector for local privilege escalation. Even in single-user or containerized environments, it can allow a compromised application to access or modify files belonging to other applications or the host system.
04Vulnerable & Fixed Patterns
Vulnerable pattern
import os
import tempfile
# Attacker can replace the file between check and use
temp_path = os.path.join(tempfile.gettempdir(), "config.txt")
# Check: is it a regular file?
if os.path.isfile(temp_path) and not os.path.islink(temp_path):
# Use: open and read (attacker replaces with symlink here)
with open(temp_path, 'r') as f:
config = f.read()
Why it's vulnerable: The isfile() and islink() checks happen at one moment, but the open() call happens later. An attacker can create a symlink at temp_path between the check and the open, causing the program to read a file the attacker designates.
Fixed pattern
import os
import tempfile
temp_path = os.path.join(tempfile.gettempdir(), "config.txt")
# Open with O_EXCL and O_NOFOLLOW to prevent symlink attacks
try:
fd = os.open(temp_path, os.O_RDONLY | os.O_NOFOLLOW)
with os.fdopen(fd, 'r') as f:
config = f.read()
except (OSError, FileNotFoundError):
# Handle error (file doesn't exist or is a symlink)
config = None
Vulnerable pattern
<?php
$temp_file = sys_get_temp_dir() . '/config.txt';
// Check: is it a regular file?
if (is_file($temp_file) && !is_link($temp_file)) {
// Use: read the file (attacker replaces with symlink here)
$config = file_get_contents($temp_file);
}
?>
Why it's vulnerable: The is_file() and is_link() checks occur before file_get_contents(). Between these calls, an attacker can replace the file with a symlink, causing the program to read an unintended target.
Fixed pattern
<?php
$temp_file = sys_get_temp_dir() . '/config.txt';
// Use realpath() to resolve and verify the file is where expected
$real_path = realpath($temp_file);
$temp_dir = realpath(sys_get_temp_dir());
// Ensure the resolved path is still within the temp directory
if ($real_path && strpos($real_path, $temp_dir) === 0 && is_file($real_path)) {
$config = file_get_contents($real_path);
} else {
// File is outside expected directory or doesn't exist
$config = null;
}
?>
05Prevention Checklist
Use atomic operations: Prefer functions that check and use in a single call (e.g., os.open() with O_NOFOLLOW and O_EXCL flags in Python, or realpath() validation in PHP) rather than separate check-then-use steps.
Avoid world-writable directories: Create temporary files in application-owned directories with restricted permissions, not in /tmp or %TEMP%, whenever possible.
Validate resolved paths: After resolving a symlink with realpath() or equivalent, confirm the final path is within the expected directory before proceeding.
Use file descriptors: Once a file is safely opened, use the file descriptor (not the path) for subsequent operations to prevent the file from being swapped.
Apply principle of least privilege: Run services and daemons with the minimum necessary permissions; avoid setuid binaries that operate on user-controlled paths.
Lock files during use: Use advisory or mandatory file locking to prevent concurrent modification between check and use.
06Signs You May Already Be Affected
Look for unexpected files appearing in temporary directories, or log entries showing file operations on paths outside the intended scope (e.g., a service that should only read from /var/app/config suddenly accessing /etc/passwd). If you run privileged processes that create or modify files in shared directories, monitor for symlinks pointing to sensitive locations in those directories.