Live › vulnerability › GHSA-j456-xh4h-cpf2
Go to dashboardNLTK's WekaClassifier passes a caller-supplied model path straight to the weka Java tool, skipping the pathsec containment checks that other NLTK wrappers apply. An application that lets an untrusted workflow choose that path lets an attacker write or overwrite files outside NLTK's data roots, and probe whether arbitrary files exist or parse as models.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-27 14:52 UTC. All times on this page are UTC.
Exposure requires an application that passes an attacker-influenced path into WekaClassifier, either as the model_filename argument to train or as the model path to the constructor. Applications that only use WekaClassifier with paths fixed in code or config, or that do not use weka at all, are not exposed.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| nltk | pypi | <= 3.10.3 | 3.10.4 |
pip show nltk and confirm the version is <= 3.10.3.WekaClassifier calls where model_filename or the model path comes from user input, a request, or another untrusted workflow.nltk.pathsec.ENFORCE to True, which is the default on current HEAD.6a1760629; the fix is merged on develop but no release contains it yet as of 2026-09-28.WekaClassifier.train or the WekaClassifier constructor as the model path; use fixed, server-chosen paths instead.Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
WekaClassifier in NLTK passes a caller-controlled model file path to the weka Java VM without any nltk.pathsec validation. WekaClassifier.train puts the caller's model_filename verbatim into the weka argv ["-d", model_filename, ...] (weka writes the trained model there), and WekaClassifier._classify_many puts self._model into ["-l", self._model, ...] (weka reads it). Under nltk.pathsec.ENFORCE=True (the default on current HEAD), NLTK's stated invariant — established by advisory CVE-2026-81726 / GHSA-8mgp-746c-j5xp — is that caller-controlled model-artifact paths must be bounded to the allowed NLTK data roots before any native tool opens them. WekaClassifier is the one java()-based tool wrapper that skips this entirely, giving an untrusted workflow arbitrary-location file write (-d) and an arbitrary file read/parse oracle (-l), defeating pathsec containment.
This is a standalone, unfixed variant of the same class as CVE-2026-81726, on a code path the CVE-2026-81726 fix never touched. That advisory enumerates 6 functions (TransitionParser.train, TransitionParser.parse, AveragedPerceptron.save, AveragedPerceptron.load, PerceptronTagger.save_to_json, save_maxent_params) — WekaClassifier is not among them, and weka.py contains no validate_tool_path/validate_model_resource at all.
WekaClassifier.train(cls, model_filename, ...) builds cmd = [javaclass, "-d", model_filename, "-t", train_filename] (nltk/classify/weka.py:256) and WekaClassifier._classify_many builds [..., "-l", self._model, "-T", <staged>, ...] (weka.py:131-133); self._model is stored unvalidated in __init__ (weka.py:108-110). Only the internally-generated train.arff/test.arff are staged via make_staging_dir; the model path is passed as-is. nltk.internals.java() (internals.py:388-431) validates the classpath (_verify_jar_sandbox), the JVM launcher options (_validate_java_options, the CVE-2026-12841 fix), the main-class token cmd[0], and rejects @argfile tokens in any position — but it explicitly permits program arguments starting with - and never inspects the interior of a program-arg path value. So -d <path> / -l <path> flow unchecked to and the weka JVM performs an unbounded native open/write on the caller-chosen path.
Sibling wrappers prove the intended pattern: CRFTagger.train/set_model_file call validate_tool_path (crf.py:134,300); MaltParser.generate_malt_command validates -i/-o via validate_tool_path (malt.py:340,344), its model via validate_model_resource (malt.py:326) and working_dir via validate_path (malt.py:206). weka.py already uses pathsec elsewhere (validate_path for WEKAHOME, pathsec_open for ARFF writes) but never for the model path.
Under pathsec.ENFORCE=True (default on HEAD), an untrusted workflow that chooses the weka model save/load path obtains:
-d — overwrite or create a file at an attacker-chosen absolute or ..-traversal path outside the allowed NLTK data roots (content constrained to weka's serialized-model format; the target choice is the integrity impact). This corrupts/clobbers arbitrary chosen files.-l — weka loads an out-of-root path as a model, yielding an existence/parse differential (file content is not echoed back → confidentiality impact is limited).This defeats the pathsec containment advisory CVE-2026-81726 promises, at the same privilege level / threat model that advisory was accepted under.
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}])
The argv construction — the vulnerable step — is confirmed without weka installed (see Bypass Evidence); a configured weka.jar is needed only to observe the JVM's actual file I/O.
pathsec.ENFORCE=True lets an untrusted workflow choose the weka model path and calls WekaClassifier.train("../../../home/victim/x", featuresets) (write) or WekaClassifier(fmt, "/home/victim/secret.model").classify_many(...) (read). Guard: none — plain classmethod/constructor parameter (weka.py:230,108-110). Bypass proof: grep shows weka.py uses no validate_tool_path/validate_model_resource/validate_tool_dir (NONE).cmd = [javaclass, "-d", model_filename, "-t", train_filename] (weka.py:256) / ["-l", self._model, ...]. Guard: only the internally-generated train.arff is staged via make_staging_dir; the model path is passed as-is. Bypass proof: runtime capture yielded ['weka.classifiers.bayes.NaiveBayes', '-d', '/tmp/outside_sandbox_weka.model', '-t', '<staged>/train.arff'] — the raw out-of-root path is cmd[2].java(cmd, classpath=_weka_classpath, ...) → subprocess.Popen(final_cmd) (internals.py:431). Guard: validates classpath, JVM , , and rejects tokens. Bypass proof: none of these inspect the / program-arg value; internals.py explicitly documents that program args after the main class may start with , and the path is neither nor an -token, so every gate passes. Negative control: the same path through raises .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'] and read-side [..., '-l', '/home/victim/secret.model', '-T', '<staged>/test.arff', '-p', '0'].git show v3.10.3:nltk/classify/weka.py contains both the -d model-path build (line 235) and the -l read, and NO validate_tool_path.<= 3.10.3 (present on the latest release tag v3.10.3 and on HEAD c06cd264). No published fix has ever targeted the weka -d/-l model path; commits after v3.10.3 (ee6d6138, 92aeada6, a9752559) touched weka.py for other sinks but did not add model-path validation.
Before building the -d/-l/-T command, validate the model path with pathsec, mirroring CRFTagger/MaltParser:
validate_tool_path(model_filename, context="WekaClassifier.train", for_write=True, must_exist=False)
validate_tool_path(self._model, context="WekaClassifier._classify_many")
Reported by zx (Jace) — GitHub: @manus-use
Status: MERGED on develop (unreleased; v3.10.3 of 2026-08-12 predates the fix).
PR: #3863 "Bound the WekaClassifier model path to the pathsec data roots", merged 2026-09-06, merge commit bc2878725 on develop. Fix commit: 6a1760629.
Probe: nltk/test/unit/security_probes/ghsa_j456_xh4h
This page as .md · .json · in your agent: get_signal("GHSA-j456-xh4h-cpf2")
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.
subprocess.Popen(final_cmd, shell=False)java()optionscmd[0]@argfile-d-l-cmd[0]@java()validate_tool_path(..., for_write=True, must_exist=False)PermissionError-d) / parse-existence oracle (-l). Guard: pathsec is never consulted (no validate_tool_path, no pathsec_open). Bypass proof: the directly-analogous sibling CRFTagger.train calls validate_tool_path(model_file, must_exist=False) (crf.py:300); weka does not, and commit ee6d6138 hardened weka's other sinks (scratch files, WEKAHOME jar) while leaving the model path unvalidated.