Oct 24, 2026·5 min read·1 visit
Unsafe string replacement in virtualenv bash activation scripts allows arbitrary command execution via crafted prompt names during environment activation.
A command injection vulnerability in PyPA virtualenv prior to version 21.14.9 allows local attackers to execute arbitrary commands when a user sources a generated shell activation script containing a crafted prompt value.
PyPA virtualenv is a standard Python component used to create isolated virtual execution environments. During environment generation, virtualenv creates shell activation scripts (such as bin/activate) that modify shell settings, environment variables, and interactive command prompts.
In versions prior to 21.14.9, caller-supplied prompt identifiers supplied through the --prompt argument or environment settings are embedded into the interactive prompt string (PS1) inside bin/activate. The mechanism used to sanitize the custom prompt string relied on incomplete naive pattern replacements.
When a user sources an activation script where the prompt contains shell expansion syntax, such as command substitution constructs $() or backticks, the interactive shell evaluates the injected syntax during prompt rendering. This results in arbitrary code execution within the security context of the user sourcing the environment.
The underlying flaw resides in src/virtualenv/activation/bash/activate.sh. When custom prompts were supplied, virtualenv attempted to sanitize special prompt characters including backslashes, dollar signs, and backticks using Bash parameter substitution.
The initial implementation performed character replacements as follows:
_VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\\/\\\\\\\\}"
_VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\$/\\\$}"
_VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\`/\\\`}"This escaping strategy was flawed because shell prompt parsing in Bash and compatible shells executes in multiple evaluation passes. First, Bash decodes standard backslash prompt escapes. Second, if promptvars is active or if the shell operates in POSIX compatibility mode, it performs parameter expansion, command substitution, and arithmetic expansion on the resulting string.
Because the escaping logic only neutralized single-pass expansions, characters that survived initial string decoding were subjected to a secondary evaluation pass. Furthermore, when non-Bash shells such as dash sourced the script, parameter substitution rules behaved differently, causing syntax errors or secondary parsing flaws.
The remediation implemented in PR #3418 (commit 5f48d711731311428aebfae61d46957762155b22) changes the mechanism used to update PS1. Rather than evaluating and escaping variable content directly into the double-quoted string, the patched version defers evaluation by storing a literal variable reference.
The patch adds checks to ensure that PS1 modifications occur only when compatible shell instances (bash or zsh) are detected, bypassing unsupported POSIX shells like dash:
--- a/src/virtualenv/activation/bash/activate.sh
+++ b/src/virtualenv/activation/bash/activate.sh
@@ -137,29 +137,26 @@ else
export TK_LIBRARY
fi
- if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
+ if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] &&
+ { [ -n "${BASH_VERSION-}" ] || [ -n "${ZSH_VERSION-}" ]; }; then
_OLD_VIRTUAL_PS1="${PS1-}"
- _VIRTUAL_PROMPT="${VIRTUAL_ENV_PROMPT}"
- if [ -n "${BASH_VERSION-}" ]; then
- _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\\/\\\\\\\\}"
- _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\$/\\\$}"
- _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\`/\\\`}"
- elif [ -n "${ZSH_VERSION-}" ]; then
- _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\\/\\\\}"
- _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\$/\\\$}"
- _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\`/\\\`}"
- fi
- if [ -n "${ZSH_VERSION-}" ]; then
- _VIRTUAL_PROMPT="${_VIRTUAL_PROMPT//\%/%%}"
+ 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
- PS1="(${_VIRTUAL_PROMPT}) ${PS1-}"
- unset _VIRTUAL_PROMPT
fiBy assigning PS1='(${VIRTUAL_ENV_PROMPT-}) '"${PS1-}", Bash treats VIRTUAL_ENV_PROMPT strictly as a literal variable reference during prompt rendering, entirely preventing shell command parsing of its inner content.
Exploitation requires an attacker to control the --prompt argument supplied during virtual environment creation or to influence the VIRTUAL_ENV_PROMPT environment variable prior to activation.
An attack scenario involves a shared repository or build configuration containing a pre-configured virtual environment generation script, where the prompt parameter is set to $(id > /tmp/pwned). When virtualenv builds the environment, it writes the raw or inadequately escaped string into bin/activate.
virtualenv --prompt '$(id > /tmp/pwned)' .venvWhen a user or automated pipeline executes source .venv/bin/activate, the shell reads the PS1 configuration. Upon rendering the primary prompt in the terminal, the subshell invocation executes id > /tmp/pwned without producing syntax errors.
Successful exploitation leads to arbitrary command execution in the context of the user running the interactive shell session. If an administrator or developer sources a compromised virtual environment activation script, the payload executes with their full privileges.
This vulnerability impacts developer workstations, continuous integration and continuous deployment (CI/CD) pipelines, and automated build scripts that dynamically construct virtual environments based on external repository metadata.
The CVSS v4.0 score is rated at 7.1 (High) with vector string 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. Exploitation requires local user interaction to source the generated script.
To resolve this vulnerability, update virtualenv to version 21.14.9 or later using standard package management tooling:
pip install --upgrade "virtualenv>=21.14.9"Existing virtual environments constructed prior to the update retain vulnerable bin/activate scripts. System administrators must regenerate existing virtual environments to ensure patched activation templates are written to disk.
Alternatively, users can set the VIRTUAL_ENV_DISABLE_PROMPT=1 environment variable prior to activation. This setting bypasses PS1 prompt modification entirely, preventing command evaluation regardless of virtualenv version.
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| Product | Affected Versions | Fixed Version |
|---|---|---|
virtualenv PyPA | < 21.14.9 | 21.14.9 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-78 |
| CVSS v4.0 Score | 7.1 (High) |
| Attack Vector | Local |
| EPSS Score | 0.00142 (3.23th percentile) |
| CISA KEV | Not Listed |
| Exploit Status | Proof of Concept Available |
| Impact | Arbitrary Code Execution |
The software constructs 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 sent to a downstream component.
virtualenv prior to version 21.14.9 contains a local permission inheritance vulnerability in its app-data copy seeder. Due to the reliance on Python's standard shutil.copy() function, permissions from cached packages are replicated directly onto newly created virtual environment files, ignoring the active process umask and allowing unprivileged local attackers to inject code.
A code injection vulnerability in pypa/virtualenv prior to version 21.14.9 allows arbitrary command execution in xonsh shells using legacy multibyte encodings when sourcing activate.xsh.
Prior to version 21.14.9, virtualenv downloads wheel files directly into its reusable persistent cache directory prior to verifying PyPI digests or compatibility. Combined with unsanitized stdout parsing from pip, a controlled package index can cause virtualenv to calculate digests against pre-existing local files while leaving unverified archives in the shared persistent cache.
A verification bypass vulnerability in virtualenv allows unverified Python wheel installation when using pip seeding with the download flag. Affected versions fail to perform SHA-256 digest validation against PyPI metadata before invoking pip.
Insecure temporary file usage (CWE-377) in virtualenv app-data seeder creates dangling symlinks pointing to deleted temporary directories, allowing local attackers on shared systems to execute arbitrary code.
A Time-of-Check Time-of-Use (TOCTOU) race condition in the Python `filelock` library (versions 4.1.0 to 4.2.0) allows local unprivileged attackers to cause arbitrary directory deletion by swapping file system targets during lock cleanup or dead owner sweeps.