GitHub Advisoryadvisory · PyPI
GHSA-cx6f-vjhg-g3cc virtualenv: Bash activator leaves the --prompt value unescaped in PS1 when promptvars is off, so prompt expansion later executes it (regression in 21.14.9)
HIGH
Official page ↗
- Aliases
- CVE-2026-115586
- Ecosystem
- PyPI
- Packages
- virtualenv
- Severity
- High
- 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
- Weaknesses
- CWE-78
- Reviewed
- 2026-10-23
Affected packages
• PyPI virtualenv: >= 21.14.9, < 21.14.13 → fixed in 21.14.13
Description
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
```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) /tmp/ve-pwn)y) /tmp/ve-pwn)y) /tmp/ve-pwn)y' /tmp/venv-demo
cat > /tmp/rc `\$`, 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
…
Related
Alias · CVE (1)
- CVE-2026-115586 virtualenv is a tool for creating isolated virtual python environments. vulnerability
Weakness · CWE (1)
- CWE-78 Improper Neutralization of Special Elements used in an OS Command weakness
References
- https://github.com/pypa/virtualenv/security/advisories/GHSA-cx6f-vjhg-g3cc web
- https://github.com/pypa/virtualenv/pull/3466 web
- https://github.com/pypa/virtualenv/commit/3598e201f6f044e389136b2347a8a919ac03c5f4 web
- https://github.com/pypa/virtualenv package
- https://github.com/pypa/virtualenv/releases/tag/21.14.13 web