{"id":"CVE-2026-113723","aliases":["GHSA-6w3r-xr62-4xcp"],"url":"https://o3.security/vulnerability/CVE-2026-113723","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->>V: --prompt x$(touch MARKER)y\n    V->>B: generated bin/activate\n    B->>B: assign escaped prompt to PS1\n    alt PS1 is not exported\n        B->>D: start sh with its own PS1\n        D-->>B: marker absent\n    else PS1 is exported\n        B->>D: start sh with inherited PS1\n        D->>D: expand $(touch MARKER) on prompt draw\n        D-->>B: 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 </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.","published":"2026-10-20T19:51:06.340Z","modified":"2026-10-24T02:48:21.041603950Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"virtualenv","fixedVersion":"21.14.9"}],"fix":{"url":"https://github.com/pypa/virtualenv/commit/5f48d711731311428aebfae61d46957762155b22","label":"pypa/virtualenv@5f48d71"},"references":[{"type":"WEB","url":"https://github.com/pypa/virtualenv/releases/tag/21.14.9"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/113xxx/CVE-2026-113723.json"},{"type":"ADVISORY","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":"FIX","url":"https://github.com/pypa/virtualenv/commit/5f48d711731311428aebfae61d46957762155b22"},{"type":"FIX","url":"https://github.com/pypa/virtualenv/pull/3418"},{"type":"PACKAGE","url":"https://github.com/pypa/virtualenv"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-24T02:48:21.041603950Z"}}