Skip to content

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 severity GitHub Reviewed Published Oct 22, 2026 in pypa/virtualenv • Updated Oct 23, 2026

Package

pip virtualenv (pip)

Affected versions

>= 21.14.9, < 21.14.13

Patched versions

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

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)", which removed the unconditional $/backtick escaping from the bash path. Advisory for that change: GHSA-6w3r-xr62-4xcp.

Proof of concept

Non-destructive payload: it runs id and writes the output to a file.

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.

    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

  • The problem reproduces on the latest virtualenv release.

References

@gaborbernat gaborbernat published to pypa/virtualenv Oct 22, 2026
Published to the GitHub Advisory Database Oct 23, 2026
Reviewed Oct 23, 2026
Last updated Oct 23, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v4 base metrics

Exploitability Metrics
Attack Vector Local
Attack Complexity High
Attack Requirements Present
Privileges Required None
User interaction Active
Vulnerable System Impact Metrics
Confidentiality High
Integrity High
Availability High
Subsequent System Impact Metrics
Confidentiality None
Integrity None
Availability None

CVSS v4 base metrics

Exploitability Metrics
Attack Vector: This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.
Attack Complexity: This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. These are conditions whose primary purpose is to increase security and/or increase exploit engineering complexity. A vulnerability exploitable without a target-specific variable has a lower complexity than a vulnerability that would require non-trivial customization. This metric is meant to capture security mechanisms utilized by the vulnerable system.
Attack Requirements: This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack. These differ from security-enhancing techniques/technologies (ref Attack Complexity) as the primary purpose of these conditions is not to explicitly mitigate attacks, but rather, emerge naturally as a consequence of the deployment and execution of the vulnerable system.
Privileges Required: This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.
User interaction: This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner.
Vulnerable System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the VULNERABLE SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the VULNERABLE SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the VULNERABLE SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
Subsequent System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the SUBSEQUENT SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the SUBSEQUENT SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the SUBSEQUENT SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
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

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(19th percentile)

Weaknesses

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. Learn more on MITRE.

CVE ID

CVE-2026-115586

GHSA ID

GHSA-cx6f-vjhg-g3cc

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.