Oct 27, 2026·10 min read·5 visits
NLTK versions up to 3.10.3 fail to validate model file paths in WekaClassifier.train() and WekaClassifier._classify_many(), bypassing the nltk.pathsec filesystem sandbox and enabling arbitrary file overwrites and local file existence probing via the Weka JVM.
CVE-2026-109956 (GHSA-j456-xh4h-cpf2) is a path traversal vulnerability and security sandbox bypass in the Python Natural Language Toolkit (NLTK) versions 3.10.3 and earlier. The WekaClassifier module fails to validate caller-controlled model file paths before passing them to the Weka Java Virtual Machine (JVM) via command-line arguments. When operating under the nltk.pathsec security sandbox, this omission permits attackers who control model path parameters to read, probe, or overwrite arbitrary files outside authorized NLTK data directories.
The Natural Language Toolkit (nltk) is a Python library for symbolic and statistical natural language processing. In addition to native Python implementations, NLTK includes wrapper classes that interface with external machine learning tools and Java binaries. One such wrapper is WekaClassifier, located in nltk/classify/weka.py, which bridges NLTK classification workflows with the Waikato Environment for Knowledge Analysis (Weka) toolkit running inside a Java Virtual Machine (JVM).
To restrict filesystem access when processing untrusted inputs or executing within multi-tenant environments, NLTK implements a filesystem security sandbox inside the nltk.pathsec module. When nltk.pathsec.ENFORCE is set to True (the default security posture), NLTK enforces an architectural invariant requiring all caller-controlled file paths, model artifacts, and corpus targets to reside within designated, permitted NLTK data roots (nltk.pathsec._get_allowed_roots()).
In NLTK versions 3.10.3 and earlier, WekaClassifier failed to enforce nltk.pathsec validation on caller-supplied model paths across both training and classification entry points. Specifically, WekaClassifier.train() and WekaClassifier._classify_many() passed unvalidated file path strings directly into the command-line arguments of the Weka JVM subprocess (-d for destination output and -l for model loading).
Classified under CWE-22 (Improper Limitation of a Pathname to a Restricted Directory), this omission completely bypasses the nltk.pathsec containment mechanism. An attacker capable of supplying or modifying the model filename argument in an application using WekaClassifier can write serialized Java model data to arbitrary filesystem locations or probe the existence and readability of sensitive local files.
The root cause of CVE-2026-109956 is an incomplete security migration during previous hardening efforts of NLTK's external tool wrappers and model loaders. In earlier security advisories, such as GHSA-8mgp-746c-j5xp (CVE-2026-81726), the maintainers hardened native Python model interfaces including TransitionParser.train, AveragedPerceptron.save, and PerceptronTagger.save_to_json. Sibling native-tool wrappers were also secured: CRFTagger enforced validate_tool_path() on model files, and MaltParser.generate_malt_command() validated its input/output flags (-i, -o) and working directories.
In contrast, nltk/classify/weka.py was only partially integrated with nltk.pathsec. While the module used validate_path() to verify the WEKAHOME environment directory and used pathsec_open() to secure internal temporary ARFF dataset files, the user-supplied model_filename parameter was overlooked in three distinct locations: class initialization (__init__), batch classification (_classify_many), and model training (train).
During model training, WekaClassifier.train() constructs a command array containing ["-d", model_filename, "-t", train_filename] and passes it to nltk.internals.java(). During classification, WekaClassifier.__init__() stores model_filename in self._model without inspection, and _classify_many() subsequently passes ["-l", self._model, "-T", test_filename] to java().
The underlying subprocess execution helper, nltk.internals.java(), performs several security checks before invoking subprocess.Popen(final_cmd, shell=False). It validates classpath JAR containment via _verify_jar_sandbox(), verifies JVM launcher options via _validate_java_options() (mitigating CVE-2026-12841), verifies the main Java class token, and blocks @argfile expansion tokens. However, java() treats all tokens following the main class name as opaque application arguments. Because -d and -l are application-level flags interpreted by weka.classifiers.* inside the JVM rather than by the java launcher itself, nltk.internals.java() passes them directly to the operating system process creation API without validating their path arguments.
In NLTK 3.10.3 and earlier, nltk/classify/weka.py imported validate_path and pathsec_open, but did not import or invoke validate_tool_path. Below is the vulnerable implementation in nltk/classify/weka.py prior to the fix:
class WekaClassifier(ClassifierI):
def __init__(self, formatter, model_filename):
self._formatter = formatter
# VULNERABLE: model_filename is stored without pathsec validation
self._model = model_filename
def _classify_many(self, featuresets, options):
config_weka()
# ...
# VULNERABLE: self._model is passed directly to Weka's -l (load) flag
cmd = [javaclass, "-l", self._model, "-T", test_filename] + options
java(cmd, classpath=_weka_classpath, stdout= stdout, stderr=stderr)
@classmethod
def train(
cls,
model_filename,
train_instances,
classifier="weka.classifiers.bayes.NaiveBayes",
options=[],
quiet=True,
):
config_weka()
# ...
# VULNERABLE: model_filename is passed directly to Weka's -d (dump/write) flag
cmd = [javaclass, "-d", model_filename, "-t", train_filename]
java(cmd, classpath=_weka_classpath)Commit 6a17606293fe9d3ea1006f99dd3fed2e2b25d519 (PR #3863) remediates the vulnerability by importing validate_tool_path from nltk.pathsec and placing validation guards at all three control points prior to invoking config_weka() or java():
@@ -22,7 +22,7 @@
from nltk.internals import config_java, java
from nltk.pathsec import ZipFile as SecureZipFile
from nltk.pathsec import open as pathsec_open
-from nltk.pathsec import validate_path
+from nltk.pathsec import validate_path, validate_tool_path
from nltk.probability import DictionaryProbDist
_weka_classpath = None
@@ -107,6 +107,10 @@ def _check_weka_version(jar):
class WekaClassifier(ClassifierI):
def __init__(self, formatter, model_filename):
self._formatter = formatter
+ # Bound the caller-controlled model path to the pathsec data roots before
+ # it reaches the weka JVM as a -l read target, closing the model-artifact
+ # containment gap weka.py was left out of (GHSA-j456-xh4h-cpf2).
+ validate_tool_path(model_filename, context="WekaClassifier", must_exist=False)
self._model = model_filename
def prob_classify_many(self, featuresets):
@@ -116,6 +120,12 @@ def classify_many(self, featuresets):
return self._classify_many(featuresets, ["-p", "0"])
def _classify_many(self, featuresets, options):
+ # Re-bound the model path in case _model was reassigned after construction;
+ # weka reads it via -l (GHSA-j456-xh4h-cpf2). Containment, not existence, is
+ # the security property, so must_exist stays False.
+ validate_tool_path(
+ self._model, context="WekaClassifier._classify_many", must_exist=False
+ )
# Make sure we can find java & weka.
config_weka()
@@ -233,6 +243,15 @@ def train(
options=[],
quiet=True,
):
+ # Bound the caller-controlled model path before weka writes to it via -d
+ # (GHSA-j456-xh4h-cpf2); it need not exist yet, and validating before any
+ # weka lookup refuses an out-of-root path early.
+ validate_tool_path(
+ model_filename,
+ context="WekaClassifier.train",
+ for_write=True,
+ must_exist=False,
+ )
# Make sure we can find java & weka.
config_weka()The patch implementation addresses potential bypass variants thoroughly:
config_weka() is called, ensuring that disallowed paths raise a PermissionError or ValueError immediately without probing for a local Java or Weka installation.validate_tool_path(self._model, ...) inside _classify_many(), the fix prevents a bypass where a caller instantiates WekaClassifier with a benign path and subsequently mutates clf._model before calling classify_many().validate_tool_path canonicalizes paths via os.path.realpath (including parent directory resolution when for_write=True and must_exist=False), blocking symlink escapes placed inside legitimate NLTK data roots. It also rejects NUL bytes (\x00), CLI option-shaped strings (such as -loadModel or --outputFormat), and URL schemes (http://, file:///). Note that a theoretical Time-of-Check to Time-of-Use (TOCTOU) window exists between Python's validate_tool_path() check and the Java process opening the file, though exploiting this requires concurrent local filesystem write access inside an already-trusted NLTK data directory.Exploiting CVE-2026-109956 requires an application that uses WekaClassifier and exposes the model_filename parameter to untrusted input (for example, an automated ML pipeline, multi-tenant notebook service, or NLP web API allowing users to specify custom model names or paths). For end-to-end execution of the file write or read oracle in the JVM, the host environment must also have Java and the weka.jar archive configured.
The vulnerability provides two primary exploitation primitives:
-d sink): When WekaClassifier.train(model_filename, train_instances) is called, the Weka JVM serializes the trained Java classifier object (ObjectOutputStream) directly to model_filename. An attacker can provide absolute paths (e.g., /etc/cron.d/evil) or relative traversal sequences (e.g., ../../../home/user/.bashrc). While the attacker only partially controls the file structure (since it is encapsulated in Java serialization headers and model parameter structures), the ability to overwrite arbitrary files owned by the application user allows corrupting critical application state, clobbering configuration files, or injecting payloads into contexts tolerant of binary headers.-l sink): When WekaClassifier(formatter, model_filename).classify_many(featuresets) is executed, Weka attempts to open and deserialize the target file. Depending on whether the target path does not exist, lacks read permissions, or fails Java stream header parsing (StreamCorruptedException: invalid stream header), Weka outputs distinct error messages to stderr, which NLTK captures and raises in a RuntimeError. This allows an attacker to probe the local filesystem layout.> [!NOTE]
> Furthermore, if an attacker can place a maliciously crafted Java serialized object anywhere on the local filesystem (for example, via a separate temporary file upload mechanism outside the NLTK data roots), they can use the -l path traversal primitive to force the Weka JVM to deserialize that untrusted file via ObjectInputStream.readObject(), potentially escalating to arbitrary code execution via Java deserialization gadgets present in the Weka classpath.
The following proof-of-concept demonstrates how both primitives bypass nltk.pathsec.ENFORCE = True in NLTK <= 3.10.3:
import nltk.pathsec as ps
from nltk.classify.weka import WekaClassifier
# Ensure the NLTK filesystem sandbox is active
ps.ENFORCE = True
# 1. Arbitrary File Write Escape via WekaClassifier.train (-d flag)
# In <= 3.10.3, validate_tool_path is omitted; Weka writes outside allowed roots.
traversal_write_target = "/tmp/outside_sandbox/evil.model"
sample_data = [({"feature1": 1}, "positive"), ({"feature1": 0}, "negative")]
WekaClassifier.train(traversal_write_target, sample_data)
# 2. Arbitrary File Read / Deserialization Sink via _classify_many (-l flag)
# Points the Weka JVM to open an unauthorized file outside NLTK data roots.
traversal_read_target = "/etc/passwd"
clf = WekaClassifier(formatter=None, model_filename=traversal_read_target)
clf.classify_many([{"feature1": 1}])The primary security impact of CVE-2026-109956 is a violation of filesystem integrity (VI:H / I:H), accompanied by limited confidentiality (VC:L / C:L) and availability (VA:L / A:L) impacts. Systems that rely on nltk.pathsec.ENFORCE = True to isolate untrusted NLP workflows or third-party model specifications lose containment when WekaClassifier is reachable.
Through the arbitrary file write vector in WekaClassifier.train(), an attacker can overwrite any file writable by the OS user running the Python process. Overwriting application databases, source files, or configuration artifacts results in targeted denial of service or persistent integrity compromise. Through the -l load vector in _classify_many(), attackers can enumerate private files outside the sandbox or trigger Java deserialization of arbitrary local files.
Vulnerability scoring reflects the architectural requirements of the flaw:
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:L). The High Attack Complexity (AC:H) reflects the requirement that the target application must explicitly expose WekaClassifier paths to external input and have a working Weka/Java environment configured.CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N), capturing the deployment prerequisite under Attack Requirements Present (AT:P).According to FIRST.org metrics, the Exploit Prediction Scoring System (EPSS) score for CVE-2026-109956 is 0.00318 (0.32%, 24.59th percentile), and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
Development and operations teams utilizing NLTK should upgrade to version 3.10.4 or later immediately. Release 3.10.4 incorporates commit 6a17606293fe9d3ea1006f99dd3fed2e2b25d519, which bounds all WekaClassifier model paths using nltk.pathsec.validate_tool_path() and adds regression tests (nltk/test/unit/test_weka_model_path_security.py) covering relative traversal, absolute paths, symlink escapes, NUL byte injection, and CLI option smuggling.
To upgrade via pip:
pip install --upgrade "nltk>=3.10.4"If an immediate package upgrade is not operationally feasible, apply the following mitigations:
WekaClassifier(formatter, model_filename) or WekaClassifier.train(model_filename, ...), explicitly invoke nltk.pathsec.validate_tool_path(model_filename, context="custom_guard", for_write=True, must_exist=False) in application code.WEKAHOME environment variable is unset and remove weka.jar from the host environment so config_weka() halts execution before spawning a JVM.nltk.pathsec.ENFORCE remains set to True (default) and audit configured nltk.data.path directories to ensure they do not contain untrusted symlinks or overly permissive write permissions.CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
nltk NLTK Project | <= 3.10.3 | 3.10.4 |
| Attribute | Detail |
|---|---|
| Vulnerability Class | CWE-22: Path Traversal / Sandbox Bypass |
| Affected Component | nltk.classify.weka.WekaClassifier (PyPI: nltk <= 3.10.3) |
| CVSS v3.1 / v4.0 | 7.0 High (NVD v3.1) / 8.3 High (VulnCheck v4.0) |
| Attack Vector | Network / Caller-Controlled Model Path Argument |
| EPSS Score | 0.32% (24.59th Percentile) |
| Security Impact | Arbitrary File Overwrite (-d) and Local File Probing (-l) |
| Exploit Status | Proof-of-Concept (Unit Test Suite Verified) |
| CISA KEV Status | Not Listed |
The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Authlib versions prior to 1.9.0 contain a Server-Side Request Forgery (SSRF) vulnerability in the RFC 9101 JWT-Secured Authorization Request (JAR) server implementation. The JWTAuthenticationRequest class passes an unauthenticated, client-controlled request_uri parameter directly to the abstract get_request_object() method before applying any destination validation or client registration policies. Because the official documentation previously recommended implementing this hook as an unrestricted HTTP fetch, deployments following those guidelines allow remote attackers possessing a valid public client_id to trigger outbound network requests to internal services and infer fetch outcomes via error response variations.
CVE-2026-35002 is an arbitrary code execution vulnerability in the Agno agent platform (formerly Phidata) affecting all versions prior to 2.3.24. The flaw resides in the model execution logic (libs/agno/agno/models/base.py) and tool serialization logic (libs/agno/agno/tools/function.py), where the application directly passes the field_type string parameter into Python's built-in eval() function to resolve string type names into Python data types. An attacker who can influence FunctionCall arguments via prompt injection or supply untrusted dictionary structures for deserialization can execute arbitrary Python expressions within the context of the host process.
CVE-2026-115895 is a high-severity code injection vulnerability (CWE-94) in the happy-dom npm package prior to version 21.0.4. The vulnerability exists within the ECMAScriptModuleCompiler component, which transforms ECMAScript module syntax into executable Node.js functions when JavaScript evaluation is enabled. Due to an incomplete fix for CVE-2026-33943, the compiler failed to validate local named import aliases against legal JavaScript identifier rules. Attackers supplying crafted module imports—specifically utilizing destructuring default initializers alongside JSON module attributes—can inject arbitrary JavaScript expressions directly into the host Node.js runtime context.
An improper input validation and authorization bypass vulnerability exists in the LangGraph storage layer affecting langgraph, langgraph-checkpoint-postgres, and langgraph-checkpoint-sqlite. Because relational store implementations serialize namespace tuples into dot-delimited strings without validating labels on read, search, delete, and batch operations, an attacker who controls a namespace label can inject periods (.) to traverse namespace boundaries and access, modify, or delete data belonging to other namespaces.
CVE-2026-115886 (GHSA-2wqp-q3c6-ccj5) is a Server-Side Request Forgery (SSRF) and Unsafe Reflection vulnerability in MLflow versions 3.12.0rc0 prior to 3.17.0. Unauthenticated REST API endpoints accept serialized third-party scorer JSON payloads that are dynamically deserialized and reflectively instantiated without sanitizing constructor keyword arguments. When optional dependencies such as trulens and trulens-providers-litellm are installed, attackers can inject egress-steering arguments into the scorer payload, forcing the MLflow server to dispatch outbound HTTP requests to arbitrary internal destinations or cloud metadata services.
CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) is a high-severity denial-of-service vulnerability affecting the multer multipart/form-data upload middleware for Node.js and Express in versions 2.0.0 through 2.4.0. When an endpoint is configured to accept multiple files using an asynchronous fileFilter callback, a crafted, truncated multipart HTTP request can trigger an unhandled error event on the underlying busboy stream parser after request teardown has already completed. Because all error listeners are stripped during teardown, Node.js treats the resulting error event as an uncaught exception and immediately terminates the server process.