{"id":"CVE-2026-113723","kind":"vulnerability","aliases":["CVE-2026-113723","GHSA-6w3r-xr62-4xcp"],"related":[],"title":"virtualenv: Bash prompt values can change meaning under older compatibility modes","description":"## 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.","summary":"A virtualenv environment created with an attacker-influenced --prompt value can have that text re-expanded as shell syntax when the environment is activated in certain shells (dash sourcing activate, an inherited exported PS1, or bash compatibility level 4.2 or lower). An attacker gets command execution with the user's privileges when the prompt is drawn.","severity":"high","cvss":7.1,"epss":0.00142,"exploited":false,"exploited_at":null,"products":[{"name":"virtualenv","fixed":["21.14.9"],"affected":"< 21.14.9","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["ci","cwe-78","developer-machines","package","pypi","python","virtualenv"],"not_affected_if":["default-config","fish-or-csh-activators","trusted-prompt-values"],"how_to_check":"- Run `virtualenv --version` and compare against `21.14.9`\n- Look for virtualenv in dependency lockfiles or `pip show virtualenv` and check the version\n- Check whether environments were created with prompt or destination values from untrusted sources","fix":"- Upgrade virtualenv to `21.14.9` or later\n- Recreate existing environments with virtualenv `21.14.9` or later\n- Until upgraded, set `VIRTUAL_ENV_DISABLE_PROMPT=1` to disable the environment prompt\n- Use only trusted prompt and destination values","status":"open","published_at":"2026-10-20T20:17:18+00:00","first_seen_at":"2026-10-23T21:57:21+00:00","triaged_at":"2026-10-25T03:34:36+00:00","updated_at":"2026-10-25T03:34:36+00:00","category":"package","confidence":0.7,"triage":{"at":"2026-10-25T03:34:36Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade virtualenv to `21.14.9` or later\n- Recreate existing environments with virtualenv `21.14.9` or later\n- Until upgraded, set `VIRTUAL_ENV_DISABLE_PROMPT=1` to disable the environment prompt\n- Use only trusted prompt and destination values","reach":"local","title":"virtualenv: crafted prompt text runs commands when bash activation prompt is drawn","impact":"high","summary":"A virtualenv environment created with an attacker-influenced --prompt value can have that text re-expanded as shell syntax when the environment is activated in certain shells (dash sourcing activate, an inherited exported PS1, or bash compatibility level 4.2 or lower). An attacker gets command execution with the user's privileges when the prompt is drawn.","category":"package","nicknames":[],"who_cares":["python","pypi","virtualenv","developer-machines","ci"],"confidence":0.7,"needs_auth":"unknown","worst_case":"Commands run with the user's privileges when the prompt is drawn.","analyst_note":"","how_to_check":"- Run `virtualenv --version` and compare against `21.14.9`\n- Look for virtualenv in dependency lockfiles or `pip show virtualenv` and check the version\n- Check whether environments were created with prompt or destination values from untrusted sources","needs_analyst":false,"not_affected_if":["trusted-prompt-values","fish-or-csh-activators","default-config"],"who_is_affected":"Teams that create virtualenv environments using prompt or destination values that come from outside the team (for example from tox or hatch configuration relayed from untrusted input) and then activate them with bash or sh. Environments created only from trusted, fixed values with the default prompt are not exposed through this path. The advisory says the issue does not establish exposure through unrelated activators.","user_interaction":"yes","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.000941,"input_tokens":6028,"output_tokens":676,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-1':626C '-113723':14A,22A '-2026':13A,21A,625C '-78':26B '/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/bash/activate.sh).':209C '/pypa/virtualenv/pull/3326)':402C '/pypa/virtualenv/pull/3418)':429C '/tmp/repro.d':645C '20':492C '20.0.0':495C '21.14.7':505C '21.14.8':200C,530C '3326':399C '3390':408C,501C '3418':426C '4.2':74B,135C,278C,377C '4xcp':19A '5.2':515C '6w3r':17A 'a857a7823c73':633C 'absent':588C 'activ':59B,65B,105C,121C,163C,171C,194C,261C,372C,389C,494C 'advisori':468C,681C 'affect':128C,196C,481C 'alt':572C 'anoth':386C 'ant':624C 'area':129C 'argument':187C 'aris':384C 'assembl':206C 'assign':234C,567C 'attack':41B,78B,108C,173C,177C,531C 'attacker-control':172C 'attacker-influenc':40B 'b':545C,562C,565C,566C,577C,586C,593C,609C 'b1':496C 'bash':2A,71B,104C,130C,204C,240C,271C,273C,326C,344C,374C,414C,478C,514C,548C 'becom':99C 'behavior':145C,667C 'bin/activate':211C,323C,564C 'boundari':518C 'branch':243C 'bug':362C,663C 'caller':94C 'caller-suppli':93C 'capabl':656C 'case':512C,646C 'certain':61B 'chang':6A,410C,424C,648C 'child':264C,310C,354C,380C,552C 'child-shel':309C,379C 'ci':24B 'claim':471C 'code':197C 'come':519C 'command':80B,258C,458C 'command-execut':457C 'commit':632C 'companion':642C 'compat':10A,72B,133C,149C,276C,375C,510C 'compatibility-level':509C 'concern':161C 'confirm':525C 'consol':367C 'contain':657C 'control':174C,178C,333C 'copi':212C 'cover':411C 'creat':37B,295C,313C,339C,356C,611C 'cve':12A,20A 'cwe':25B 'd':550C,578C,585C,594C,600C,601C,608C 'dash':63B,259C,265C,331C,355C,371C,487C,553C 'data':182C,256C,442C 'demonstr':507C 'destin':181C 'develop':28B 'developer-machin':27B 'differ':146C 'direct':262C,370C,486C 'display':294C,345C 'dispos':653C 'draw':125C,137C,224C,607C 'drawn':91B,444C 'els':589C 'end':360C,612C 'entrypoint':640C 'environ':36B,57B,123C,139C,297C,315C,387C 'escap':244C,568C 'establish':167C 'everi':473C,477C 'evid':305C,674C 'execut':81B,100C,349C,459C 'expand':51B,219C,440C,602C 'expans':238C 'expect':641C 'export':68B,268C,343C,391C,576C,592C 'exposur':168C 'file':614C,617C,643C,670C 'find':397C 'first':491C,502C 'follow':406C,616C 'follow-up':405C 'generat':193C,563C 'get':79B 'ghsa':16A 'ghsa-6w3r-xr62-4xcp':15A 'github.com':208C,401C,428C 'github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/bash/activate.sh).':207C 'github.com/pypa/virtualenv/pull/3326)':400C 'github.com/pypa/virtualenv/pull/3418)':427C 'give':365C 'histor':517C 'id':650C 'imag':637C 'includ':672C 'influenc':42B,110C 'inherit':67B,266C,498C,598C 'input':175C 'interact':325C,329C,547C 'interpret':158C,254C 'introduc':482C 'keep':430C 'last':659C 'later':190C,237C,456C 'leav':151C,334C,417C 'level':73B,134C,277C,376C,511C 'line':661C 'link':395C 'lower':76B,280C 'machin':29B 'maintain':465C,528C 'marker':287C,320C,340C,357C,559C,587C,604C,610C 'mean':7A 'mechan':210C 'mermaid':533C 'mode':11A,150C,479C 'moi2x3t':627C 'must':109C,119C,230C 'need':651C 'neither':337C 'newer':148C 'occur':668C 'older':9A 'output':660C 'packag':30B 'particip':535C,540C,544C,549C 'pass':113C,183C 'path':252C,382C,415C,420C 'payload':302C 'posix':162C,474C 'privat':446C 'privileg':86B 'prompt':3A,43B,89B,96C,106C,127C,140C,152C,179C,205C,214C,226C,248C,255C,293C,317C,347C,437C,538C,556C,569C,606C 'propos':423C 'ps1':69B,216C,220C,269C,335C,342C,392C,435C,499C,571C,573C,584C,590C,599C 'pypi':31B 'python':32B 're':50B 're-expand':49B 'refer':433C 'releas':503C,524C 'relev':199C 'replac':143C 'replacement-str':142C 'report':160C,364C,447C,484C,522C,622C,666C 'repro':639C 'reproduc':312C,359C,449C,613C 'reproduct':303C,368C 'rerun':678C 'result':460C 'review':466C 'root':655C 'root-cap':654C 'rule':249C 'run':131C,274C,290C,300C,451C 'scanner':307C,394C,454C,621C,636C 'script':195C 'second':157C 'separ':366C 'sequenc':532C 'sequencediagram':534C 'sh':330C,580C,596C 'shell':53B,62B,218C,311C,338C,381C,475C 'sourc':64B,191C,201C,260C,322C,488C 'start':328C,579C,595C 'still':253C 'string':144C 'subject':154C 'subsequ':120C 'summari':92C 'suppli':95C,462C 'supplier':539C 'surviv':231C 'syntax':54B,101C 'test':630C 'text':48B,97C,153C,438C 'three':250C,419C 'touch':286C,291C,319C,558C,603C 'trace':485C 'unexport':336C 'unrel':170C 'updat':682C 'user':84B,118C,189C,649C 'usual':247C,413C 'v':541C,555C,561C 'valu':4A,44B,112C,229C,282C,351C 'variabl':432C 'virtualenv':1A,23A,33B,35B,115C,186C,543C 'without':348C 'x':285C,318C,493C,557C 'xr62':18A 'y':288C,321C,560C 'zsh':242C","nicknames":[],"mitigations":[],"who_affected":"Teams that create virtualenv environments using prompt or destination values that come from outside the team (for example from tox or hatch configuration relayed from untrusted input) and then activate them with bash or sh. Environments created only from trusted, fixed values with the default prompt are not exposed through this path. The advisory says the issue does not establish exposure through unrelated activators.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-20T20:17:18+00:00","kev":null,"cvss_vector":"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","cvss_by":"security-advisories@github.com","poc_count":0,"sources":[{"source":"nvd","external_id":"CVE-2026-113723","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-113723","severity":null,"published_at":"2026-10-20T20:17:18+00:00","modified_at":"2026-10-22T16:47:38+00:00","first_seen_at":"2026-10-23T21:57:21+00:00","fetched_at":"2026-10-23T22:28:58+00:00","note":null},{"source":"ghsa","external_id":"GHSA-6w3r-xr62-4xcp","url":"https://github.com/advisories/GHSA-6w3r-xr62-4xcp","severity":"high","published_at":"2026-10-23T22:21:57+00:00","modified_at":"2026-10-23T22:21:58+00:00","first_seen_at":"2026-10-23T22:22:36+00:00","fetched_at":"2026-10-23T22:27:42+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}