CVE-2026-113714: virtualenv · Downloaded wheel selection and caching precede verification
Affects hatchSource GCVE Database
Not known to be exploited · CVSS 2.3 Low : upgrade to 1.18.1_2 when convenient.
Data as of 27 Oct 2026, 06:43 UTC
- Patch
- Available – fixed in 1.18.1_2
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:L/VI:L/VA:L/SC:N/SI:N/SA:NExploitation evidence
Disclosed: evidence found.
CVE published 20 Oct 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponised: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by Mirage: no evidence on record.
No attack traffic recorded by OffSeq Mirage sensors
A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 26 Oct 2026.
What to do
Upgrade hatch to 1.18.1_2.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
Homebrewhatchpkg:brew/hatchOther threats | Affected
| Fixed in
|
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
Description
## Summary Wheel downloads go directly into the reusable wheelhouse, while the verification target is selected from pip's textual output. A verification failure does not remove the downloaded file from that cache. Anthropic's OSS Scanner reported this as `ANT-2026-ST00DRO3` in `virtualenv-20261015-005`, received on 15 October 2026. ## Affected area Download selection and promotion into the reusable wheel cache. ## Attacker-controlled input A download must fail verification, or the archive selected from pip output must differ from the file later reused. Default-index digest verification must be enabled. Existing custom-index exceptions are outside the promised PyPI comparison. ## Affected code The relevant 21.14.8 source is in [wheel download and digest check](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/seed/wheels/acquire.py). ## Mechanism `download_wheel()` tells pip to write directly into the reusable `wheel/house` cache. It then selects a path to hash from the first `Saved ` line in pip's stdout, rather than from the file pip actually placed there. A package index can inject such a line through a discarded link's `data-requires-python` attribute. A separate failure exists without the forged line: a digest mismatch raises after the downloaded wheel has entered the cache, and the rejected bytes remain there. Later wheel selection does not hash the cached file again. ## Reproduction and evidence The scanner used a local index configured in `pip.conf`, with PyPI digest comparison still enabled. In the control case it served a modified pip wheel alone. `virtualenv --upgrade-embed-wheels` refused the mismatch, and a subsequent environment used the bundled pip. In the attack case the index added a link whose `data-requires-python` contained a forged `Saved <path>` line pointing at a genuine bundled wheel. The upgrade then exited without a digest error, recorded the modified wheel in its update log, and a later environment's `pip --version` ran a line added by the index. The reproduction used a known absolute path to the genuine bundled wheel; that prerequisite is part of this variant. The report also shows that a refused wheel can replace a previously accepted file of the same name in `wheel/house`, leaving rejected bytes available for later use. It does not claim to bypass TLS or affect the default PyPI setup. The gap follows the digest check added in #3251, first released in 21.7.12. [PR #3430](https://github.com/pypa/virtualenv/pull/3430) downloads to a private temporary directory, verifies the actual archive, and promotes it only after acceptance. ## Attack sequence ```mermaid sequenceDiagram participant I as Configured index participant P as pip download participant C as wheel/house cache participant V as virtualenv digest check participant N as New environment I->>P: modified wheel and forged Saved line P->>C: write modified wheel directly P-->>V: stdout names genuine bundled wheel V->>V: hash genuine wheel and accept V->>C: record modified wheel for reuse C->>N: seed modified pip without rehash Note over P,C: Even a rejected wheel can remain in the cache ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-ST00DRO3`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import atexit import hashlib import json import os import shutil import subprocess import sys from pathlib import Path companions = Path("/tmp/repro.d") work = Path("/tmp/forged-saved-line") home = work / "home" app_data = home / ".cache" / "virtualenv" this_python = f"{sys.version_info.major}.{sys.version_info.minor}" # the target; each Python has its own update log update_log_of_pip = app_data / "wheel" / this_python / "embed" / "3" / "pip.json" virtualenv = [sys.executable, companions / "virtualenv_with_local_pypi_json.py", work / "pypi"] as_user = {"cwd": home, "env": {"HOME": str(home), "PATH": os.environ["PATH"]}, "capture_output": True, "text": True} added_code_prints = "code added by the index" # the line index_serves_modified_pip.py adds to pip/__init__.py def user_upgrades_seed_wheels_and_creates_env(): # each case starts from an empty app-data shutil.rmtree(app_data, ignore_errors=True) shutil.rmtree(home / "env", ignore_errors=True) upgrade = subprocess.run([*virtualenv, "--upgrade-embed-wheels"], **as_user) # --no-periodic-update: the background update is another process, which the PyPI stand-in does not reach subprocess.run([*virtualenv, "--no-periodic-update", "env"], **as_user, check=True) pip_version = s
References
- github.com/pypa/virtualenv/security/advisories/GHSA-494p-63j8-28p3 (opens in a new tab)
- nvd.nist.gov/vuln/detail/CVE-2026-113714 (opens in a new tab)
- github.com/pypa/virtualenv/pull/3430 (opens in a new tab)
- github.com/pypa/virtualenv/commit/7babec66e043e1cc8bc66869a063653898719d96 (opens in a new tab)
- github.com/pypa/virtualenv (opens in a new tab)
- github.com/pypa/virtualenv/releases/tag/21.14.9 (opens in a new tab)