# virtualenv activate runs commands from the environment name in zsh with PROMPT_SUBST

virtualenv's bash activate script injects the unescaped environment name into PS1; under zsh with PROMPT_SUBST, `$(...)` in the name executes on every prompt draw.

- Payout: Research
- Severity: high
- Program: virtualenv
- Found by: kemrec
- Disclosed: 2026-10-21
- CVE: CVE-2026-113726
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-5vjq-rrrf-7h2q
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-5vjq-rrrf-7h2q
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:command-injection vuln:rce

**Target:** virtualenv activate shell script (PS1 prompt)
**Root cause:** Since 21.13.0 the activate script escapes backslashes, dollar signs and backticks in the environment name only when BASH_VERSION is set, so zsh receives the raw name; zsh with PROMPT_SUBST performs parameter/command/arithmetic expansion on PS1 at each prompt draw.
**Steps:**
1. Create env with a command-substitution name: `virtualenv 'x$(touch PWNED)y'` (or pass the value via `--prompt` / `VIRTUALENV_PROMPT`)
2. In a zsh session with `setopt prompt_subst` (default on for oh-my-zsh), run `source 'x$(touch PWNED)y/bin/activate'`
3. Prompt renders as `(xy)` and `PWNED` is created; payload re-runs on every prompt draw
**How found:** Reporter noticed the escaping added in 21.13.0 was gated on BASH_VERSION, leaving zsh unescaped, and that zsh's PROMPT_SUBST expands PS1; also observed `%n` in a name being interpreted as a zsh prompt escape, revealing the user name.
**Exploitation:** Attacker influences the environment directory name or prompt value (CI job/wrapper passing a branch, project or job name; a directory in a cloned repo or unpacked archive; `--prompt .` taking the cwd name). Victim sources the activate script in zsh with PROMPT_SUBST enabled, executing the embedded `$(...)`/backtick/`${...}` payload in their shell once per prompt.
**Insight:** When a fix adds escaping behind a shell-version check, audit the other shells the same script serves: zsh's PROMPT_SUBST makes PS1 a code-execution sink, and oh-my-zsh enables it by default, so any user-controlled value reaching PS1 is RCE.

