# NLTK: WekaClassifier passes an unvalidated caller-controlled model path to the weka JVM, bypassing nltk.pathsec containment

NLTK's WekaClassifier passes caller-controlled model paths to the weka JVM via -d/-l, bypassing pathsec containment for arbitrary-location file write and read oracle.

- Payout: Research
- Severity: high
- Program: NLTK
- Found by: manus-use
- Disclosed: 2026-10-27
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-j456-xh4h-cpf2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-j456-xh4h-cpf2
- Tags: area:ai domain:ai domain:oss impact:integrity kind:research platform:ghsa sev:high tech:java tech:python technique:incomplete-fix technique:source-review vuln:path-traversal

**Target:** WekaClassifier model path argument to weka JVM (nltk/classify/weka.py)
**Root cause:** WekaClassifier.train and _classify_many place the caller-supplied model_filename/self._model verbatim into weka argv (-d/-l) without validate_tool_path, while nltk.internals.java() only validates classpath, JVM options, cmd[0] and @argfile tokens, never program-arg path values.
**Steps:**
1. Set `nltk.pathsec.ENFORCE = True`.
2. Call `WekaClassifier.train("/tmp/outside_sandbox/evil.model", featuresets)` — argv becomes `['weka.classifiers.bayes.NaiveBayes', '-d', '/tmp/outside_sandbox_weka.model', '-t', '<staged>/train.arff']`.
3. Or construct `WekaClassifier(fmt, "/home/victim/secret.model")` and call `classify_many(...)` — argv contains `'-l', '/home/victim/secret.model'`.
4. Observe weka JVM writes/reads the out-of-root path; negative control `validate_tool_path(..., for_write=True, must_exist=False)` raises PermissionError.
**How found:** Compared WekaClassifier against sibling wrappers CRFTagger and MaltParser, which call validate_tool_path/validate_model_resource; grepped weka.py and found no such validation, then captured the constructed argv to confirm the raw path reaches java() unchecked.
**Exploitation:** An app enforcing pathsec that lets an untrusted workflow choose the weka model path gets arbitrary-location file write (content constrained to weka serialized-model format) via -d, and an existence/parse oracle via -l; no content echo, so read confidentiality is limited.
**Insight:** When a project adds a path-containment layer, audit every native-tool wrapper for the same sink class — sibling wrappers that call the validator reveal which ones were missed; also note that java()'s argfile/option gates deliberately allow program args starting with '-', so path values after the main class are never inspected.

## Proof of concept (copied exactly from the original report)

**PoC: WekaClassifier writes/reads model outside sandbox**

```
import nltk.pathsec as ps; ps.ENFORCE = True
from nltk.classify.weka import WekaClassifier
# write-side: model written OUTSIDE any allowed NLTK data root (refused for CRFTagger, allowed here)
WekaClassifier.train("/tmp/outside_sandbox/evil.model",
                     [({"a": 1}, "pos"), ({"a": 0}, "neg")])
# read-side: -l opens an arbitrary path
clf = WekaClassifier(fmt, "/home/victim/secret.model")
clf.classify_many([{"a": 1}])
```

