馃寪 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

馃悰 fix(bash): preserve literal prompts in supported shells - #3418

Merged
gaborbernat merged 8 commits into
pypa:mainfrom
gaborbernat:fix/shell-prompt-selection
Oct 17, 2026
Merged

gaborbernat merged 8 commits into
pypa:mainfrom
gaborbernat:fix/shell-prompt-selection

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

The POSIX activate script puts the environment name in parentheses in front of PS1. bash, and zsh under PROMPT_SUBST, expand PS1 each time they draw the prompt, so the script escaped backslashes, dollar signs and backticks for those two shells and assigned the plain name in every other shell. ksh93 expands PS1 the same way and got the plain name, so a name holding $(...) ran as a command at every draw there; the test that sources the script in bash with BASH_VERSION unset shows that path. Under bash the escaping depended on the shell's own settings: it was built for the two passes bash runs with promptvars on, and the stray backslashes showed when the option was off, while under BASH_COMPAT 4.2 or lower, the level bash 3.2 on macOS runs at, the escaping produced one backslash where two were meant and a name ending in \\ lost a character. 馃悰 The script now changes PS1 only under bash and zsh. Under bash it stores a reference to VIRTUAL_ENV_PROMPT whenever bash expands the prompt, and otherwise doubles backslashes and nothing else. This builds on #3326, on the fix for GHSA-6w3r-xr62-4xcp, and on #3390 and #3392, which escaped the name for both of bash's passes.

PS1 was assigned in every shell

The prompt block checked only VIRTUAL_ENV_DISABLE_PROMPT. Its bash branch (#3390) and its zsh branch (the advisory fix) escaped the name, and the assignment that followed ran in every shell, as it has since #1454. dash does not expand PS1, so it showed the plain name; ksh93 does, so it ran whatever the name held. The guard now also requires BASH_VERSION or ZSH_VERSION. Other shells keep their PS1; they still get VIRTUAL_ENV_PROMPT exported for their own prompt code, and the dash test checks that PS1 comes back untouched.

flowchart LR
    A[activate sourced] --> S{shell}
    S -- bash --> B[escaped for bash]
    S -- zsh --> Z[escaped for zsh]
    S -- other --> O[plain name into PS1]
    O --> K[ksh93 expands it at every draw]
    S -- other, now --> U[PS1 untouched, VIRTUAL_ENV_PROMPT exported]
    classDef bad fill:#ffd6d6,stroke:#c1121f,color:#5c0000;
    classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332;
    class O,K bad;
    class U ok;
Loading

The bash escaping assumed one expansion mode

bash draws PS1 in two passes, described under Controlling the Prompt. It first decodes backslash escapes, turning \\ into \ and \$ into $ or #. Then, when the promptvars option is set, it runs parameter expansion, command substitution, arithmetic expansion and quote removal on the result. In POSIX mode, entered through --posix, set -o posix, POSIXLY_CORRECT or invocation as sh, the second pass runs regardless of promptvars. The old bash branch from #3390 escaped the name once for each pass, in every mode. With promptvars off and no POSIX mode only the decode ran, so the prompt showed \$, \` and doubled backslashes. The escapes came from ${name//pattern/replacement} substitutions whose replacement strings undergo quote removal only after bash 4.2; at BASH_COMPAT 4.2 or lower the same substitutions yielded half the backslashes, so a name holding \\ rendered as \.

The bash branch now tests shopt -q promptvars || shopt -qo posix, the two conditions under which the second pass runs, and stores '(${VIRTUAL_ENV_PROMPT-}) ' in front of the old prompt. The decode pass finds no backslash in that prefix. The expansion pass substitutes the variable's value, and bash does not scan the text a parameter expansion produced for further expansions, so the name is drawn as it is at any compatibility level, and no substitution on the name takes place at activation time. The - default keeps a set -u session drawing () after unset VIRTUAL_ENV_PROMPT instead of failing with unbound variable. When neither condition holds, the other branch doubles backslashes for the decode pass and leaves $ and backticks alone, since no expansion follows; zsh keeps its $, backtick and % escapes in the same branch. One test sources a --prompt 'x$(touch PWNED)y' environment with promptvars off, with promptvars off under set -o posix, and under set -u with the variable unset, and another renders backslash names at BASH_COMPAT 3.1, 4.2 and 5.1.

sequenceDiagram
    participant A as activate
    participant P as PS1
    participant B as bash draw
    rect rgb(255, 214, 214)
    A->>P: before: name escaped for both passes
    B->>B: decode, backslash escapes removed
    B->>B: promptvars off: stop, stray backslashes drawn
    end
    rect rgb(216, 243, 220)
    A->>P: now, promptvars or posix: '(${VIRTUAL_ENV_PROMPT-}) '
    B->>B: decode, no backslash to act on
    B->>B: expand, variable value inserted and not rescanned
    end
    rect rgb(207, 228, 255)
    A->>P: now, neither: name with backslashes doubled
    B->>B: decode only, each pair drawn as one backslash
    end
Loading

Behavior changes

Under bash, $PS1 after activation reads (${VIRTUAL_ENV_PROMPT-}) followed by the previous prompt, rather than the escaped name; the drawn prompt is the same. Changing VIRTUAL_ENV_PROMPT after activation changes the drawn prefix, and unsetting it draws (), also under set -u. Turning promptvars off after activation outside POSIX mode draws the literal (${VIRTUAL_ENV_PROMPT-}). A session activated with promptvars off and outside POSIX mode holds the name in PS1 with only its backslashes doubled; turning promptvars on afterwards expands that text, so a name holding $(...) runs there, where the old form stayed literal. In that same branch at BASH_COMPAT 4.2 or lower, a backslash followed by a prompt escape letter, such as \n, is decoded, because the doubling substitution keeps one backslash there. dash, ksh and every other shell that is not bash or zsh keep their PS1; VIRTUAL_ENV_PROMPT is still exported for them. zsh keeps its escaping, and without PROMPT_SUBST it still shows \$ and \\ literally, as it did before this PR. Existing activation scripts need regeneration.

Reported by Anthropic's OSS Scanner: ANT-2026-1MOI2X3T.

@gaborbernat
gaborbernat force-pushed the fix/shell-prompt-selection branch from 944b8ff to 37ba203 Compare October 16, 2026 14:23
@gaborbernat gaborbernat added the security Fixes a vulnerability or hardens the supply chain, CI or release label Oct 16, 2026
@gaborbernat
gaborbernat marked this pull request as draft October 16, 2026 19:04
@gaborbernat gaborbernat changed the title 馃悰 fix(bash): limit prompt changes to supported shells 馃悰 fix(bash): preserve literal prompts in supported shells Oct 17, 2026
Reported by Anthropic's OSS Scanner: ANT-2026-1MOI2X3T.
Bash expands PS1 in posix mode even with promptvars disabled, so the
literal branch let a $(...) in the prompt run at every draw when sourced
from sh or under set -o posix. Use the variable reference whenever bash
expands the prompt, fold the prompt block into one branch per expansion
mode, and have the dash test check PS1 stays untouched.
@gaborbernat
gaborbernat force-pushed the fix/shell-prompt-selection branch from e439b3b to c571d5b Compare October 17, 2026 09:30
@gaborbernat
gaborbernat marked this pull request as ready for review October 17, 2026 09:35
@gaborbernat
gaborbernat merged commit 5f48d71 into pypa:main Oct 17, 2026
74 checks passed
@gaborbernat
gaborbernat deleted the fix/shell-prompt-selection branch October 17, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug security Fixes a vulnerability or hardens the supply chain, CI or release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant