NLTK: WekaClassifier passes an unvalidated caller-controlled model path to the weka JVM, bypassing nltk.pathsec containmentGHSA-j456-xh4h-cpf2
HIGHFix: nltk/nltk#3863GHSA-j456-xh4h-cpf2 is a high-severity (CVSS 8.6) Path Traversal vulnerability in nltk. A fix is available for nltk — see the affected versions and patch details below.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-j456-xh4h-cpf2 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 396,257 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
nltkReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Summary
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.
Root Cause
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 subprocess.Popen(final_cmd, shell=False) 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.
Impact
Under pathsec.ENFORCE=True (default on HEAD), an untrusted workflow that chooses the weka model save/load path obtains:
- Arbitrary-location file write via
-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. - Arbitrary file read/parse oracle via
-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.
Proof of Concept
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.
Attack Chain
- Entry. Action: an app running with
pathsec.ENFORCE=Truelets an untrusted workflow choose the weka model path and callsWekaClassifier.train("../../../home/victim/x", featuresets)(write) orWekaClassifier(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 novalidate_tool_path/validate_model_resource/validate_tool_dir(NONE). - Command build. Action: the path is placed into
cmd = [javaclass, "-d", model_filename, "-t", train_filename](weka.py:256) /["-l", self._model, ...]. Guard: only the internally-generatedtrain.arffis staged viamake_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 iscmd[2]. - java() sink. Action:
java(cmd, classpath=_weka_classpath, ...)→subprocess.Popen(final_cmd)(internals.py:431). Guard:java()validates classpath, JVMoptions,cmd[0], and rejects@argfiletokens. Bypass proof: none of these inspect the-d/-lprogram-arg value; internals.py explicitly documents that program args after the main class may start with-, and the path is neithercmd[0]nor an@-token, so everyjava()gate passes. Negative control: the same path throughvalidate_tool_path(..., for_write=True, must_exist=False)raisesPermissionError. - Impact. Action: the weka JVM opens the path outside the allowed roots → arbitrary-location write (
-d) / parse-existence oracle (-l). Guard: pathsec is never consulted (novalidate_tool_path, nopathsec_open). Bypass proof: the directly-analogous siblingCRFTagger.traincallsvalidate_tool_path(model_file, must_exist=False)(crf.py:300); weka does not, and commitee6d6138hardened weka's other sinks (scratch files, WEKAHOME jar) while leaving the model path unvalidated.
Bypass Evidence
- Negative control (guard is real):
validate_tool_path("/tmp/outside_sandbox_weka.model", for_write=True, must_exist=False)→PermissionError;validate_tool_path("../../../home/victim/.ssh/authorized_keys", ...)→PermissionError. - Sink reached unvalidated (guard absent): captured write-side argv
['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']. - Version:
git show v3.10.3:nltk/classify/weka.pycontains both the-dmodel-path build (line 235) and the-lread, and NOvalidate_tool_path.
Affected Versions
<= 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.
Suggested Fix
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
Fix status (2026-09-28)
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_cpf2.py, added by PR #3905 "Add GHSA-j456 live probe; expand the GHSA-8mgp probe attack set" (commit a96715264, merged 2026-09-20, merge commit 84d987bf8); verdict on develop 2fa3f48fe: FIXED (WekaClassifier -l and -d refuse out-of-root model paths).
Release: none yet; no release tag contains 6a1760629.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | nltk | all versions | 3.10.4pip install --upgrade 'nltk==3.10.4' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for nltk, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update nltk to 3.10.4 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-j456-xh4h-cpf2 is resolved across your whole dependency graph.
Workarounds
Resolve every user-supplied path to its canonical form and reject anything that escapes the intended directory, and run the component under an account that has no read or write access outside the directory it legitimately serves.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
A moderate path traversal vulnerability was found in the Natural Language Toolkit (NLTK) WekaClassifier module, which affects Red Hat AI offerings that ship the nltk Python package, including Red Hat Lightspeed and OpenShift AI (RHOAI) components. The WekaClassifier does not validate the model_filename and model path…
Mitigation for this issue is either not available or the currently available options do not meet the Red Hat Product Security criteria comprising ease of use and deployment, applicability to widespread installation base, or stability.Source: Red Hat security advisory for GHSA-j456-xh4h-cpf2 (CC BY 4.0)
Frequently Asked Questions
Is GHSA-j456-xh4h-cpf2 in your dependencies?
Find it across PyPI, including transitive dependencies.