virtualenv: Bashアクティベータにおいて、`promptvars`が無効の場合、`--prompt`の値が`PS1`でエスケープされずに設定され、その結果、後続のプロンプト展開処理で実行されてしまう(21.14.9での回帰)。

## 概要

Bashアクティベータは、`promptvars`が無効な場合に、呼び出し元から提供されたプロンプトの値を`PS1`にエスケープせずに設定し、その後でプロンプト展開処理が実行されることはないと想定しています。`promptvars`とPOSIXモードは、実行時のシェル状態であり、アクティベーションの時点の属性ではありません。同じシェルでプロンプト展開を再度有効にする(`shopt -s promptvars`、`set -o posix`)か、エクスポートされた`PS1`を継承するインタラクティブな子Bashプロセスを開始すると、Bashは保存された値を展開し、その中のコマンド置換を実行します。攻撃者がプロンプトの値を制御できる場合、`bin/activate`を実行するユーザーの権限でコマンドを実行できてしまいます。

これは回帰です。21.14.8では、`$`とバッククォートが常にエスケープされ、同じシーケンスでは何も実行されませんでした。21.14.9の修正(GHSA-6w3r-xr62-4xcp、#3418)でその部分が書き換えられ、`promptvars`が無効な場合に、Bashが元の値を保存する状態になりました。

## 影響範囲

アクティベーションスクリプト(`activate`、`activate.bat`、`activate.csh`、`activate.fish`、`activate.nu`、`Activate.ps1`、`activate.xsh`)—特にBashの`bin/activate`。

## 攻撃者によって制御される入力

`--prompt`の値。これは、セキュリティポリシーで、ユーザーが記述していないプロジェクトのメタデータから提供される可能性がある値としてリストされています。この値は、コマンドラインで`--prompt`として、またはAPIの`prompt`引数として`virtualenv`に渡され、`bin/activate`にリテラルな`VIRTUAL_ENV_PROMPT`の代入として書き込まれます(正しく引用されており、値はデータとして渡されます)。その後、スクリプトは`$`やバッククォートをエスケープせずに、この値を`PS1`にコピーします。

例:リポジトリまたはツールの設定で、`x$(id > /tmp/ve-pwn)y`のようなプロンプトが提供されます。被害者は、その環境を作成し、後でアクティベートします。

## 詳細

最新版のソースコードの場所: [`src/virtualenv/activation/bash/activate.sh` の 140 行目から 160 行目](https://github.com/pypa/virtualenv/blob/21.14.12/src/virtualenv/activation/bash/activate.sh#L140-L160)

```sh
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] &&
{ [ -n "${BASH_VERSION-}" ] || [ -n "${ZSH_VERSION-}" ]; }; then
_OLD_VIRTUAL_PS1="${PS1-}"
# bash では、promptvars が有効になっている場合や posix モードの場合、PS1 はプロンプトのエスケープシーケンスがデコードされた後に展開されます。そのため、プロンプトは変数への参照を持ち、その変数の値は展開に到達しません。それ以外の場合は、デコードのみが行われ、バックスラッシュは 2 倍にする必要があります。...
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
```

2 つの問題が組み合わさっています。

1. 実行時のシェルオプションが定数として扱われています。`source` コマンド実行時に `shopt -u promptvars` が実行されると、シェル全体でその設定が維持されるわけではありません。`shopt -s promptvars` と `set -o posix` は、同じシェルで PS1 の展開を再度有効にします。コード自体は `shopt -qo posix` をテストしており、これは posix モードでは `promptvars` の設定に関わらずプロンプトが展開されることを示しており、アクティベーションによって展開の状態が固定されないことを示しています。
2. bash の場合、そのモードで値を変更できないようにするためのエスケープ処理がありません。GHSA-5vjq-rrrf-7h2q ("activate runs commands from the environment name in zsh with PROMPT_SUBST") は、zsh でこの問題を修正し、「ユーザーはアクティベーション後に設定できる」という理由で、`$` とバックスラッシュをエスケープしました。bash での同様のエスケープ処理は、zsh の機能が追加されたコミットと同じコミットで削除されました。

表示されるプロンプトは、この問題を直接示しています。

21.14.8 PS1 = (x\\$(id > /tmp/ve-pwn)y) <- エスケープ済み、無効
21.14.9 PS1 = (x$(id > /tmp/ve-pwn)y) <- 生の文字列
21.14.12 PS1 = (x$(id > /tmp/ve-pwn)y) <- 生の文字列

関連するコミット: [`5f48d711` "fix(bash): preserve literal prompts in supported shells (#3418)"](https://github.com/pypa/virtualenv/commit/5f48d711731311428aebfae61d46957762155b22)。このコミットは、bashのパスからの無条件の`$`またはバッククォートのエスケープを削除しました。この変更に関する注意: [GHSA-6w3r-xr62-4xcp](https://github.com/pypa/virtualenv/security/advisories/GHSA-6w3r-xr62-4xcp)。

## 概念実証

非破壊ペイロード: `id`コマンドを実行し、その出力をファイルに書き込みます。

```bash
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

# プロンプトにはttyが必要; script(1)がそれを提供します
printf 'exit\n' | script -qec 'bash --noprofile --rcfile /tmp/rc -i' /dev/null

cat /tmp/ve-pwn
```

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)
```

プロンプトは`(xy)`として表示されました。21.14.8で同じスクリプトを実行すると、ファイルは作成されません。一方、21.14.9および21.14.12ではファイルが作成されます。

トリガーはすべてpty上で実行され、アクティベーション自体は何も実行しません。実行は、プロンプトの展開が有効になった後の次のプロンプトの描画時に行われます。

| `. bin/activate`の実行後 (promptvarsを`shopt -u promptvars`で有効にした場合) | 21.14.8 | 21.14.9 | 21.14.12 |
| --- | --- | --- | --- |
| `shopt -s promptvars` | 無効 | 実行 | 実行 |
| `set -o posix` | 無効 | 実行 | 実行 |
| `export PS1`を実行し、インタラクティブな子プロセス`bash`を開始する (その`promptvars`のデフォルトはオン) | 無効 | 実行 | 実行 |
| 制御: デフォルトオプションでアクティベート | 無効 | 無効 | 無効 |
| 制御: `set -o posix`がすでに有効な状態でアクティベート | 無効 | 無効 | 無効 |

3つ目のトリガーは、明示的なオプションの変更を必要としません。汚染された`PS1`変数は引き継がれ、子bashプロセスは最初のプロンプトでそれを展開します。

`shopt -u promptvars`は、高価なbashプロンプトを安価にするための公式な方法であり、その理由で`~/.bashrc`ファイルに記述されています。また、一部のプロンプトラッパーによって設定される状態でもあります。このような環境を読み込むユーザーは、`PS1`変数にエスケープされていない値を受け取ります。

## 影響

`bin/activate`を読み込むユーザーとしてコマンドが実行されます。この際、ユーザーが記述していない値(プロジェクトのメタデータやツールの設定情報が`--prompt`オプションに渡される場合など)が使用される可能性があります。この値は、`pyvenv.cfg`ファイルと生成されたスクリプトにも保存されるため、それを生成したアクティベーションプロセスが終了した後も残り、別のシェルセッションで、プロンプトの展開が有効になっている状態で、最初のプロンプト表示時に再び実行されます。特権昇格は、アクティベーションを実行したユーザーの範囲内に留まります。

深刻度: 高。これは、アクティベーション後にシェルオプションを切り替えることで発生する、同じ種類のプロンプト展開によるコマンドインジェクション(GHSA-5vjq-rrrf-7h2q)と一致します。`promptvars`の状態をシェル条件ではなく、呼び出し元が制御する条件とみなす場合は、深刻度は中程度となります。いずれの場合でも、修正方法は同じです。

## テストされたバージョン

virtualenv 21.14.12と21.14.9(影響あり)、virtualenv 21.14.8(影響なしのコントロール)、CPython 3.14.7、Linux 6.x (WSL2)、bash 5.1.16(1)-release。`--prompt`オプションの扱いは、プラットフォームに依存しません。zsh/fish/csh/nu/PowerShellのアクティベーターは、今回の報告の対象ではありませんでした。

## 既知の対策

ユーザー向け:`promptvars`をデフォルト値のままにしてアクティベートするか、アクティベートする前に`VIRTUAL_ENV_DISABLE_PROMPT=1`を設定します。これにより、プロンプト全体がスキップされます。信頼できない値を`--prompt`オプションに渡さないでください。

アクティベーターの修正:bashにおいて、`$`とバッククォートをエスケープします。これは、両方のブランチで、単一のバックスラッシュを使用して行われます (`$` -> `\$`, バッククォート -> ``\` ``, `\` -> `\\`)。bash 5.1.16で検証した結果、`promptvars`がオン、オフ、およびオフからPOSIXモードへの移行のいずれの場合でも、このように構築された`PS1`変数は機能せず、画面にはリテラル値が表示されます。これは、現在の実装よりも優れています。現在の実装では、21.14.8の4段階のエスケープ処理により、画面上に不要なバックスラッシュが残るため、この部分を書き直しました。

```sh
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-}"
```

テストについては、`tests/unit/activation/test_bash.py::test_bash_prompt_options`では、すでに`("before", "after", "expected")`というパラメータが設定されており、`before="shopt -u promptvars"`と`after=""`がカバーされています。不足しているのは、移行に関するパラメータです。具体的には、`("shopt -u promptvars", "shopt -s promptvars", ...)`と`("shopt -u promptvars", "set -o posix", ...)`です。既存の`PWNED`チェックでは、ファイルが作成されていないことを確認しています。

## 最新リリース

- [x] この問題は、最新のvirtualenvリリースでも再現されます。

影響を受けるパッケージ:
- pip (Python) virtualenv: 影響を受けるバージョンは21.14.9以上、21.14.13未満。21.14.13で修正済み。