NLTK: `WekaClassifier`は、検証されていない、呼び出し元が制御するモデルのパスをwekaのJVMに渡しており、`nltk.pathsec`による保護機能を迂回する。
## 概要
NLTKの`WekaClassifier`は、`nltk.pathsec`による検証を行わずに、呼び出し元が制御するモデルファイルパスをwekaのJava仮想マシンに渡します。`WekaClassifier.train`メソッドは、呼び出し元の`model_filename`をそのままwekaの引数`["-d", model_filename, ...]`に含めます(wekaはここで学習済みのモデルを*書き込み*ます)。一方、`WekaClassifier._classify_many`メソッドは、`self._model`を引数`["-l", self._model, ...]`に含めます(wekaはここでモデルを*読み込み*ます)。`nltk.pathsec.ENFORCE=True`(現在のHEADにおけるデフォルト設定)の場合、NLTKが主張する原則(アドバイザリCVE-2026-81726 / GHSA-8mgp-746c-j5xpで示されている)は、呼び出し元が制御するモデルファイルパスは、ネイティブツールがそれらを開く前に、許可されたNLTKのデータルートに制限される必要があるというものです。`WekaClassifier`は、この原則を完全に無視した、Javaベースのツールラッパーの唯一のものです。これにより、信頼できないワークフローにおいて、任意の場所へのファイル**書き込み**(`-d`)と、任意のファイルの**読み込み/解析**(`-l`)が可能になり、`pathsec`による保護機能が無効化されます。
これは、CVE-2026-81726と同じクラスの、未修正のバリアントであり、CVE-2026-81726の修正が適用されなかったコードパスに存在します。そのアドバイザリでは、6つの関数(`TransitionParser.train`、`TransitionParser.parse`、`AveragedPerceptron.save`、`AveragedPerceptron.load`、`PerceptronTagger.save_to_json`、`save_maxent_params`)が列挙されていますが、`WekaClassifier`は含まれておらず、weka.pyには`validate_tool_path`または`validate_model_resource`といった検証機能は一切存在しません。
## 根本原因
`WekaClassifier.train(cls, model_filename, ...)` は `cmd = [javaclass, "-d", model_filename, "-t", train_filename]` を構築します (nltk/classify/weka.py:256)。また、`WekaClassifier._classify_many` は `[..., "-l", self._model, "-T", <staged>, ...]` を構築します (weka.py:131-133)。`self._model` は `__init__` で検証されずに保存されます (weka.py:108-110)。内部で生成される `train.arff` / `test.arff` ファイルのみが `make_staging_dir` を使用してステージングされます。モデルのパスはそのまま渡されます。`nltk.internals.java()` (internals.py:388-431) は、クラスパス (`_verify_jar_sandbox`)、JVM の起動オプション (`_validate_java_options`、CVE-2026-12841 の修正)、メインクラスのトークン `cmd[0]` を検証し、どの位置にも `@argfile` トークンを拒否します。しかし、`-` で始まるプログラム引数は明示的に許可され、プログラム引数のパスの内容は一切検証されません。したがって、`-d <path>` / `-l <path>` は検証されずに `subprocess.Popen(final_cmd, shell=False)` に渡され、weka JVM は呼び出し元が選択したパスに対して、制限のないネイティブなファイルオープン/書き込み操作を実行します。
関連するラッパー関数は、意図されたパターンを示しています。`CRFTagger.train` / `set_model_file` は `validate_tool_path` を呼び出します (crf.py:134,300)。`MaltParser.generate_malt_command` は `-i` / `-o` を `validate_tool_path` を使用して検証します (malt.py:340,344)、モデルを `validate_model_resource` を使用して検証します (malt.py:326)、そして `working_dir` を `validate_path` を使用して検証します (malt.py:206)。`weka.py` は、すでに他の場所でパスのセキュリティ機能を使用しています (`validate_path` は WEKAHOME 用、`pathsec_open` は ARFF の書き込み用) が、モデルのパスに対しては使用していません。
## 影響
`pathsec.ENFORCE=True` (HEAD版でのデフォルト設定) の場合、信頼できないワークフローが、Wekaモデルの保存/読み込みパスを選択すると、以下のことが起こりえます。
- `-d` オプションを介した、任意の場所へのファイル **書き込み** — 攻撃者が指定した絶対パスまたは `..` を使用したパスで、許可されたNLTKデータルート外のファイルの上書きまたは作成(ファイルの内容はWekaのシリアライズされたモデル形式に制限されます。影響はデータの完全性)。これにより、任意のファイルが破壊/上書きされます。
- `-l` オプションを介した、任意のファイル **読み込み/解析** — Wekaが、ルートディレクトリ外のパスにあるファイルをモデルとして読み込むと、ファイルが存在するかどうかの違いや解析結果の違いが生じます(ファイルの内容が反映されないため、機密性への影響は限定的)。
これは、同じ権限レベル/脅威モデルで公開されたCVE-2026-81726という脆弱性対策の有効性を無効にします。
## 実証コード
```python
import nltk.pathsec as ps; ps.ENFORCE = True
from nltk.classify.weka import WekaClassifier
# 書き込み側: 許可されたNLTKデータルート外にモデルを書き込む(CRFTaggerでは拒否されるが、ここでは許可される)
WekaClassifier.train("/tmp/outside_sandbox/evil.model",
[({"a": 1}, "pos"), ({"a": 0}, "neg")])
# 読み込み側: -l オプションで任意のパスを開く
clf = WekaClassifier(fmt, "/home/victim/secret.model")
clf.classify_many([{"a": 1}])
```
このコマンドライン引数の構築(脆弱な部分)は、Wekaがインストールされていない状態でも確認できます(詳細は「回避の証拠」を参照)。設定済みの `weka.jar` は、JVMの実際のファイルI/Oを観察するために必要です。
## 攻撃経路
1. **侵入点:** アクション: `pathsec.ENFORCE=True` で動作するアプリケーションが、信頼できないワークフローに WEKA モデルのパスを選択させ、`WekaClassifier.train("../../../home/victim/x", featuresets)` (書き込み) または `WekaClassifier(fmt, "/home/victim/secret.model").classify_many(...)` (読み込み) を呼び出す。 対策: なし - 単純なクラスメソッド/コンストラクタのパラメータ (weka.py:230, 108-110)。 回避の証明: `grep` コマンドで確認した結果、weka.py は `validate_tool_path`/`validate_model_resource`/`validate_tool_dir` を使用していない (NONE)。
2. **コマンド構築:** アクション: パスが `cmd = [javaclass, "-d", model_filename, "-t", train_filename]` (weka.py:256) / `["-l", self._model, ...]` に配置される。 対策: `make_staging_dir` によってステージングされるのは、内部で生成された `train.arff` のみで、モデルのパスはそのまま渡される。 回避の証明: 実行時のキャプチャ結果は `['weka.classifiers.bayes.NaiveBayes', '-d', '/tmp/outside_sandbox_weka.model', '-t', '<staged>/train.arff']` であり、ルートディレクトリ外のパスは `cmd[2]` に含まれている。
3. **java() シンク:** アクション: `java(cmd, classpath=_weka_classpath, ...)` → `subprocess.Popen(final_cmd)` (internals.py:431)。 対策: `java()` は、クラスパス、JVM のオプション、`cmd[0]` を検証し、`@argfile` トークンを拒否する。 回避の証明: これらの検証は、`-d`/`-l` のプログラム引数の値を検査しない。 internals.py は、メインクラスの後のプログラム引数は `-` で始まる可能性があることを明示的に記述しており、パスは `cmd[0]` でも `@` トークンでもないため、すべての `java()` の検証を通過する。 負の制御: 同じパスを `validate_tool_path(..., for_write=True, must_exist=False)` を通すと、`PermissionError` が発生する。
4. **影響:** アクション: WEKA の JVM が、許可されたルートディレクトリ外のパスを開く → 任意の場所への書き込み (`-d`) / 存在確認 (`-l`)。 対策: `pathsec` は参照されない ( `validate_tool_path` は呼び出されない、`pathsec_open` は呼び出されない)。 回避の証明: 類似の関数 `CRFTagger.train` は `validate_tool_path(model_file, must_exist=False)` を呼び出す (crf.py:300) が、WEKA は呼び出さない。 また、コミット `ee6d6138` で WEKA の他のシンク (一時ファイル、WEKAHOME jar) が強化された。
モデルのパスの検証が行われないままになっている。
## 脆弱性の検証
- 正常な状態(ガードが存在):`validate_tool_path("/tmp/outside_sandbox_weka.model", for_write=True, must_exist=False)` → `PermissionError`; `validate_tool_path("../../../home/victim/.ssh/authorized_keys", ...)` → `PermissionError`.
- 検証が行われない状態で脆弱性が存在する(ガードがない):書き込み側の引数 `['weka.classifiers.bayes.NaiveBayes', '-d', '/tmp/outside_sandbox_weka.model', '-t', '<staged>/train.arff']` と、読み込み側の引数 `[..., '-l', '/home/victim/secret.model', '-T', '<staged>/test.arff', '-p', '0']` が取得された。
- バージョン:`git show v3.10.3:nltk/classify/weka.py` には、`-d` オプションによるモデルパスの指定(235行目)と、`-l` オプションによる読み込み処理が含まれており、`validate_tool_path` は存在しない。
## 影響を受けるバージョン
`<= 3.10.3` (最新のリリースタグ v3.10.3 および HEAD `c06cd264` に存在する)。weka の `-d`/`-l` オプションによるモデルパスの脆弱性に対する修正は、これまで公開されたことがない。v3.10.3 以降のコミット (`ee6d6138`, `92aeada6`, `a9752559`) は、weka.py の他の箇所に影響を与えたが、モデルパスの検証を追加していない。
## 推奨される修正
`-d`/`-l`/`-T` コマンドを構築する前に、pathsec を使用してモデルパスを検証し、`CRFTagger`/`MaltParser` と同様にする:
```python
validate_tool_path(model_filename, context="WekaClassifier.train", for_write=True, must_exist=False)
validate_tool_path(self._model, context="WekaClassifier._classify_many")
```
---
報告者:**zx (Jace)** — GitHub: @manus-use
## 修正状況 (2026年9月28日)
状況:develop ブランチで MERGED 済み(未リリース。修正は 2026年8月12日の v3.10.3 より前)。
PR: #3863 "Bound the WekaClassifier model path to the pathsec data roots",2026年9月6日にマージ。マージコミット:bc2878725。修正コミット:6a1760629。
テスト:nltk/test/unit/security_probes/ghsa_j456_xh4h_cpf2.py。PR #3905 "Add GHSA-j456 live probe; expand the GHSA-8mgp probe attack set" (コミット a96715264、2026年9月20日にマージ、マージコミット 84d987bf8) によって追加。develop ブランチでの結果:FIXED (WekaClassifier -l および -d は、ルートディレクトリ外のモデルパスを拒否する)。
リリース情報:まだなし。リリースバージョンには6a1760629が含まれていません。
影響を受けるパッケージ:
- pip (Python) nltk: 影響を受けるバージョンは3.10.3以下、3.10.4で修正済み。