Oct 24, 2026·5 min read·5 visits
virtualenv < 21.14.9 uses shutil.copy() when seeding environments from the app-data cache, preserving original file modes and bypassing process umask. If the cache was populated under a loose umask, resulting virtual environment files may remain world- or group-writable, allowing local privilege escalation via code injection.
virtualenv prior to version 21.14.9 contains a local permission inheritance vulnerability in its app-data copy seeder. Due to the reliance on Python's standard shutil.copy() function, permissions from cached packages are replicated directly onto newly created virtual environment files, ignoring the active process umask and allowing unprivileged local attackers to inject code.
virtualenv is a standard utility in Python ecosystem workflows used to create isolated execution environments. To minimize overhead and network operations during setup, virtualenv utilizes an app-data disk cache that stores pre-downloaded wheel files and seed packages.
When creating new environments, virtualenv copies these cached artifacts into the destination directory structure. Prior to version 21.14.9, the copy mechanism preserved the exact file permission mode bits (stat.st_mode) of the cached source files. This design ignored the current process's file creation mask (umask).
If the centralized app-data cache was populated when a loose umask (e.g., 000 or 002) was active, cached files inherited permissive file mode bits such as 0666 or 0777. When subsequent environments were constructed, these permissive mode bits were retained on executable scripts and package modules, exposing the virtual environment to unauthorized local modifications.
The root cause stems from reliance on shutil.copy() and shutil.copytree() in virtualenv's synchronization logic (src/virtualenv/util/path/_sync.py). Under POSIX systems, shutil.copy() delegates mode setting to shutil.copymode(), which explicitly reads the source file's st_mode and applies it to the target file via os.chmod().
# Vulnerable path handling prior to 21.14.9
def copy(src: Path, dest: Path) -> None:
LOGGER.debug("copy %s", _Debug(src, dest))
method = copytree if src.is_dir() else shutil.copy
method(str(src), str(dest))By executing shutil.copymode(), virtualenv explicitly overrode the operating system's normal process file creation mask behavior. Files created with standard file-creation syscalls respect the active process umask by calculating mode & ~umask. In contrast, shutil.copymode() retroactively applies permission bits directly from the source stat structure.
This architecture created a cross-context permission leak. The security boundary of a newly created virtual environment was bound to the permissions of historical cache generation rather than the calling process's active system policy.
To resolve the vulnerability in commit 35f0b33562790176dd186d0a3c517af3acc7a0e1, the maintainers eliminated standard shutil.copy() calls in favor of a low-level custom file transfer routine _copy_file().
# Fixed copy implementation in src/virtualenv/util/path/_sync.py
def _copy_file(src: Path, dest: Path) -> None:
ensure_safe_to_do(src, dest)
mode: Final = 0o755 if src.stat().st_mode & (S_IXUSR | S_IXGRP | S_IXOTH) else 0o644
flags: Final = os.O_WRONLY | os.O_CREAT | os.O_EXCL | (os.O_BINARY if os.name == "nt" else 0)
# Creation applies the current umask without changing the process-wide setting.
with src.open("rb") as source, os.fdopen(os.open(str(dest), flags, mode), "wb") as target:
shutil.copyfileobj(source, target, length=1024 * 1024)The fix establishes base permission defaults of 0o755 for executable source files and 0o644 for regular files. It then invokes os.open() with os.O_CREAT | os.O_EXCL, allowing the kernel's native file creation mechanism to apply the active process umask directly against these base permission modes.
In addition, enforcing os.O_EXCL prevents symlink redirection attacks during target environment creation. If a file or symlink already exists at the destination path, the file creation operation aborts instantly.
Exploitation requires multi-user context on a local system where virtual environments are created in shared or traversable directory locations, such as /tmp or shared project spaces.
An attacker identifies a target environment containing group- or world-writable files (such as /tmp/env/lib/python3.X/site-packages/pip/__init__.py). The attacker appends arbitrary Python code to the file.
When the victim executes pip or invokes Python within the target virtual environment, the modified module is imported and executed within the security context of the victim account.
Successful exploitation grants a local attacker code execution under the privileges of the target environment's owner. If the target user possesses higher privilege or access to sensitive credentials, the attacker can hijack those session rights.
Under CVSS v4.0 metrics, the vulnerability yields a base score of 4.4 (CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:P/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N). Integrity impact on the vulnerable system is evaluated as High due to full modification rights over Python executable packages.
The attack vector is restricted to local system access, and attack complexity is classified as High because specific environment caching and filesystem permissions conditions must be satisfied prior to creation.
The primary remediation strategy is upgrading virtualenv to version 21.14.9 or later, which enforces process umask evaluation during file creation.
To audit and remediate existing virtual environments created by vulnerable versions, administrators should scan for overly permissive files in active virtual environment paths.
# Identify world-writable files inside a target virtual environment
find /path/to/venv -type f -perm -0002 -exec ls -l {} +
# Correct permissions across an existing environment
find /path/to/venv -type d -exec chmod 755 {} +
find /path/to/venv -type f -exec chmod 644 {} +
find /path/to/venv/bin -type f -exec chmod 755 {} +CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:P/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
virtualenv pypa | < 21.14.9 | 21.14.9 |
| Attribute | Detail |
|---|---|
| Vulnerability ID | CVE-2026-113720 |
| CWE ID | CWE-732 (Incorrect Permission Assignment for Critical Resource) |
| CVSS v4.0 Score | 4.4 (Medium) |
| CVSS Vector | CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:P/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N |
| Attack Vector | Local |
| Exploit Status | No public weaponized exploit available |
| CISA KEV Status | Not listed |
| Patched Version | 21.14.9 |
The software specifies permissions for a security-critical resource in a way that allows unintended access to that resource.
A command injection vulnerability in PyPA virtualenv prior to version 21.14.9 allows local attackers to execute arbitrary commands when a user sources a generated shell activation script containing a crafted prompt value.
A code injection vulnerability in pypa/virtualenv prior to version 21.14.9 allows arbitrary command execution in xonsh shells using legacy multibyte encodings when sourcing activate.xsh.
Prior to version 21.14.9, virtualenv downloads wheel files directly into its reusable persistent cache directory prior to verifying PyPI digests or compatibility. Combined with unsanitized stdout parsing from pip, a controlled package index can cause virtualenv to calculate digests against pre-existing local files while leaving unverified archives in the shared persistent cache.
A verification bypass vulnerability in virtualenv allows unverified Python wheel installation when using pip seeding with the download flag. Affected versions fail to perform SHA-256 digest validation against PyPI metadata before invoking pip.
Insecure temporary file usage (CWE-377) in virtualenv app-data seeder creates dangling symlinks pointing to deleted temporary directories, allowing local attackers on shared systems to execute arbitrary code.
A Time-of-Check Time-of-Use (TOCTOU) race condition in the Python `filelock` library (versions 4.1.0 to 4.2.0) allows local unprivileged attackers to cause arbitrary directory deletion by swapping file system targets during lock cleanup or dead owner sweeps.