Oct 24, 2026·5 min read·5 visits
virtualenv versions prior to 21.14.9 suffer from cache poisoning due to unvalidated wheel writes into shared storage and stdout parsing vulnerabilities during package downloads.
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.
The virtualenv package is a Python utility maintained by the Python Packaging Authority (PyPA) for generating isolated Python execution environments. During environment creation, virtualenv retrieves support dependencies such as pip, setuptools, and wheel. Downloaded binaries are cached in a shared, persistent local directory at wheel/house to accelerate subsequent environment creations.
In releases prior to version 21.14.9, virtualenv writes package downloads directly into this shared persistent cache before performing cryptographic digest validation or environment compatibility checks. Furthermore, virtualenv determines which file was downloaded by parsing text returned on standard output by pip during execution.
This architectural sequence creates a vulnerability to local cache poisoning when virtualenv interacts with a controlled or compromised package index. An attacker can manipulate output lines to bypass hash checks and commit unverified package archives into persistent storage.
The core flaw exists in virtualenv/seed/wheels/acquire.py inside the download_wheel() function. When fetching seed packages, virtualenv executes pip download passing the permanent wheel/house folder directly via the -d argument. Network responses are written straight into persistent storage before any cryptographic verification occurs.
To identify the target file for hash verification, _find_downloaded_wheel() parses pip stdout line by line, searching for prefixes such as Saved or File was already downloaded . When rendering log output for HTML index pages, pip echoes certain HTML tag attributes, including data-requires-python. A custom or malicious package index serving HTML responses can embed arbitrary text within these attributes.
When pip download processes an HTML link containing an attribute formatted like data-requires-python="Saved /path/to/genuine/wheel.whl", pip writes that text to standard output. virtualenv parses this line and targets the local, genuine file for SHA-256 digest validation. Consequently, verification succeeds against the local file while the unverified archive downloaded from the package index remains stored in wheel/house. In addition, when digest verification fails normally, virtualenv raises an exception but leaves the unverified binary on disk.
In vulnerable versions prior to 21.14.9, download_wheel() invokes pip download with direct destination output pointing to the permanent cache:
# Vulnerable implementation in acquire.py:
cmd = [
sys.executable, "-c", PIP_RUN, "download",
"--progress-bar", "off",
"--only-binary=:all:",
"--no-deps",
"--python-version", for_py_version,
"-d", str(to_folder), # Direct write into permanent cache
to_download,
]The fix introduced in Pull Request #3430 and commit 7babec66e043e1cc8bc66869a063653898719d96 refactors the workflow to download packages into an isolated temporary directory created with tempfile.TemporaryDirectory(dir=to_folder, prefix="download-"). The stdout parsing logic was replaced with direct filesystem inspection using folder.iterdir(), enforcing strict file count and specifier rules.
The patched implementation performs verification while the binary resides in staging:
# Fixed implementation in virtualenv 21.14.9:
with TemporaryDirectory(dir=to_folder, prefix="download-") as staging:
cmd: Final = [
sys.executable, "-c", PIP_RUN, "download",
"--progress-bar", "off", "--disable-pip-version-check",
"--only-binary=:all:", "--no-deps",
"--python-version", for_py_version,
"-d", staging, # Isolated temporary staging directory
to_download,
]
process: Final = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE, universal_newlines=True, encoding="utf-8")
out, err = process.communicate()
if process.returncode != 0:
raise CalledProcessError(process.returncode, cmd, output=out, stderr=err)
# Inspect downloaded file directly from directory
result: Final = _downloaded_wheel(Path(staging), distribution, version_spec)
# Verify digest and Python compatibility in isolation
if verify_against_pypi:
verify_wheel_digest(result)
if not result.support_py(for_py_version):
raise RuntimeError(f"downloaded wheel {result.name} does not support Python {for_py_version}")
# Atomically promote verified archive to persistent destination
destination: Final = to_folder / result.name
result.path.replace(destination)
return Wheel(destination)Additionally, get_pip_install_cmd in virtualenv/seed/embed/pip_invoke.py was updated to supply --find-links pointing to a private temporary directory populated strictly with selected wheels while passing --no-index to block arbitrary index lookups during environment seeding.
Exploiting CVE-2026-113714 requires that a user run virtualenv while configured to query an attacker-controlled or compromised package index. This scenario occurs in enterprise networks using custom package mirrors, internal index servers, or modified PIP_INDEX_URL settings.
In the forged output vector, the malicious index returns HTML containing anchor attributes designed to inject stdout strings, such as data-requires-python="Saved /absolute/path/to/trusted.whl". When pip prints the attribute text to stdout, virtualenv parses the line and calculates the cryptographic digest of the existing trusted file on disk.
Meanwhile, pip download writes the malicious package binary into wheel/house. Because digest verification succeeded against the trusted path, no exception is triggered during download processing. On subsequent executions of virtualenv, the local discovery routine find_compatible_in_house() retrieves the unverified malicious wheel from wheel/house and installs it without re-initiating PyPI digest verification.
The operational impact of this vulnerability is local cache poisoning, tracked under CWE-354 (Improper Validation of Integrity Check Value). Successful exploitation allows an attacker to inject unverified Python binaries into the local virtualenv cache shared across environments on the host.
CVSS v4.0 scores this issue at 2.3 (Low) with vector CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N. Attack complexity is rated High because it requires controlling index HTTP responses and obtaining knowledge of valid target filesystem paths. User interaction is required to initiate virtualenv execution against the custom index.
Despite the Low base CVSS score, the issue presents heightened risk in shared build infrastructure, multi-tenant CI/CD runners, and developer environments utilizing internal package mirrors. A poisoned persistent cache directory affects subsequent build jobs on the same machine, leading to arbitrary code execution inside created virtual environments.
To resolve CVE-2026-113714, upgrade virtualenv to version 21.14.9 or higher. The update replaces standard output parsing with directory inspection and isolates unverified downloads in temporary staging folders prior to atomic promotion.
Organizations running vulnerable versions of virtualenv that interact with custom package indices should clear local cache directories to eliminate unverified artifacts. Executing virtualenv --clear-app-data or deleting ~/.cache/virtualenv/wheel/house removes stored binaries.
For distribution maintainers unable to update to 21.14.9 immediately, backporting commit 7babec66e043e1cc8bc66869a063653898719d96 from PR #3430 mitigates the flaw by enforcing temporary directory staging and atomic replacement.
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:L/VI:L/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-113714 / GHSA-494p-63j8-28p3 |
| CWE ID | CWE-354 (Improper Validation of Integrity Check Value) |
| CVSS v4.0 Score | 2.3 (Low) |
| Attack Vector | Network (AV:N/AC:H/AT:P/PR:N/UI:P) |
| EPSS Score | 0.00212 (10.91th percentile) |
| Exploit Status | Unproven / None |
| CISA KEV Status | Not Listed |
| Patched Version | virtualenv 21.14.9 |
The software computes an integrity check value for data, but fails to validate or incorrectly validates the value prior to storing, caching, or utilizing the data.
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.
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.
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.
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.