SB20261105112 - Multiple vulnerabilities in Natural Language Toolkit
Published: November 5, 2026
Breakdown by Severity
- Low
- Medium
- High
- Critical
Description
This security bulletin contains information about 12 vulnerabilities.
1) Path traversal (CVE-ID: N/A)
CWE-ID: CWE-22 - Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
CVSSv4: 8.8 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N]
The vulnerability allows an attacker controlling a WekaClassifier model path in Natural Language Toolkit (NLTK) to bypass NLTK data-root containment and create or overwrite files outside the allowed roots, or cause Weka to read and attempt to parse files outside those roots. Writes contain Weka serialized-model data but can corrupt attacker-selected files. Reads expose an existence or parsing differential rather than returning file contents, limiting the confidentiality impact.
The vulnerability exists due to missing model-path validation in nltk/classify/weka.py. WekaClassifier.train passes the caller's model_filename verbatim as the Java program's -d argument, causing Weka to write the trained model at that location. The constructor stores the model path without validation, and WekaClassifier._classify_many passes it as the -l argument, causing Weka to load it. A remote attacker can supply an absolute or parent-directory traversal path through an application that lets an untrusted workflow choose the model save or load path, directing these operations outside the allowed NLTK data roots even when nltk.pathsec.ENFORCE is true.
Only internally generated training and test ARFF files are staged; the model path is not checked by validate_tool_path or validate_model_resource. The Java wrapper validates the classpath, JVM launcher options, and main-class token and rejects argument-file tokens, but does not validate path values supplied as program arguments. Consequently, these paths reach the Weka JVM through subprocess.Popen with shell execution disabled; exploitation does not require shell injection. A configured Weka JAR is required for the actual file operations, which run with the application's filesystem permissions.
2) Path traversal (CVE-ID: N/A)
CWE-ID: CWE-22 - Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
CVSSv4: 6.9 [CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:L/SC:N/SI:N/SA:N]
The vulnerability allows an attacker who controls model-path input to Natural Language Toolkit's BllipParser to cause the application process to open and read attacker-selected filesystem resources outside the allowed NLTK data directories. Targets can include local configuration or credential files readable by the process. Disclosure of file contents depends on how the application exposes parser errors or results, while file existence, size, and open behavior can provide an oracle. Selecting a FIFO, socket, device, directory, or oversized regular file can instead cause blocking or unbounded memory consumption, reducing performance or stalling the worker performing the parse.
The vulnerability exists due to missing pathsec validation in nltk/parse/bllip.py before caller-controlled paths are passed to the native bllipparser extension. The constructor's parser_model, reranker_features, and reranker_weights arguments reach load_parser_model() or load_reranker_model() without confinement to NLTK data roots. A remote attacker can exploit an application that accepts remotely supplied model-path input and forwards it to these APIs, provided the optional bllipparser package is installed. The alternative from_unified_model_dir() entry point is also affected: the third-party get_unified_model_parameters() function derives model paths without NLTK data-root validation, and those paths subsequently reach the same unchecked constructor and native loaders.
The native extension performs its own operating-system-level opens and reads to interpret the selected resources as model data. NLTK's centralized nltk.pathsec validation provides validate_tool_path() for precisely this external-tool access boundary, but BllipParser does not invoke it. Consequently, this path bypasses the name-safety checks that reject option-shaped, URL-shaped, and traversal inputs, as well as protections against symbolic links and files that are not regular and single-linked. It also bypasses model-size restrictions. Exploitation requires attacker influence over a model path passed by the application; merely using BllipParser with trusted paths does not establish that condition. The demonstrated primitive is control over which filesystem resource the process opens and reads, rather than unconditional disclosure of its contents to the attacker.
3) Path traversal (CVE-ID: N/A)
CWE-ID: CWE-22 - Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
CVSSv4: 0 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N]
The vulnerability allows arbitrary local file reads and writes outside the directories permitted by Natural Language Toolkit's filesystem security mechanism. An attacker who can influence the tab_dir argument to load_maxent_params() or save_maxent_params() can bypass filesystem containment, reading model parameter files from unauthorized directories or creating and overwriting parameter files outside approved roots. The practical impact depends on how the application exposes these APIs and the filesystem permissions of the NLTK process.
The vulnerability exists due to both functions constructing filesystem paths from caller-controlled tab_dir values and accessing those paths with Python's built-in open() without NLTK's pathsec validation. A remote attacker can supply an outside-root directory when an application exposes control of tab_dir through these APIs. The loading function reads files from the selected directory, while the saving function creates or overwrites files there. These operations bypass the intended directory restrictions even when pathsec.ENFORCE is enabled.
The affected functions are implemented in nltk/classify/maxent.py. The loading operation constructs paths including <tab_dir>/weights.txt, <tab_dir>/mapping.tab, <tab_dir>/labels.txt, and <tab_dir>/alwayson.tab; the saving operation writes model parameter files beneath the supplied directory. Thus, the attacker controls the directory used for these parameter-file operations rather than an unrestricted filename. Applications relying on nltk.pathsec to restrict access to permitted filesystem roots are exposed when attacker-influenced directories reach these functions.
4) Inefficient Algorithmic Complexity (CVE-ID: N/A)
CWE-ID: CWE-407 - Inefficient Algorithmic Complexity
CVSSv4: 8.7 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N]
The vulnerability allows an unauthenticated remote attacker to cause denial of service through sustained single-threaded CPU exhaustion in Natural Language Toolkit (NLTK) applications that load attacker-supplied CoNLL semantic-role-labeling corpora and iterate them with ConllCorpusReader.srl_instances(). A crafted corpus of roughly one megabyte can block the processing thread for tens of seconds, with larger inputs extending processing to minutes. No user interaction is required, and confidentiality and integrity are not affected.
The vulnerability exists due to repeated nested scanning in ConllCorpusReader._get_srl_instances: for each of P predicates, the builder searches up to P span columns containing R spans each, resulting in O(P^2 * R) work. A remote attacker can submit a well-formed CoNLL block through an application's corpus-ingestion surface, control its predicate and span-column counts, and place the sole head span in the last span column to force full scans of earlier columns for every predicate. The search stops only upon finding a V or C-V span containing the predicate word; the inner break and loop continuation therefore do not prevent repeated traversal under this arrangement. Blocks are read whole without a size cap, and grid validation checks only uniform column counts rather than bounding predicates, spans, or total work.
The affected implementation is in nltk/corpus/reader/conll.py and is reached through the SRL-specific API when processing untrusted data with words, pos, tree, and srl columns. Applications that never call srl_instances() are not exposed to this failure case. The path-containment sandbox restricts file locations but not processing cost, while the regular-expression wall-clock timeout covers regex matching rather than this pure-Python loop. Constant-time range-membership checks do not eliminate the excessive work because they are repeated for every traversed span.
5) Inefficient Algorithmic Complexity (CVE-ID: N/A)
CWE-ID: CWE-407 - Inefficient Algorithmic Complexity
CVSSv4: 8.7 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N]
The vulnerability allows an unauthenticated remote attacker to cause sustained single-threaded CPU exhaustion and denial of service in applications that normalize attacker-controlled parse trees using the Natural Language Toolkit (NLTK). A single wide, flat tree can block the processing thread for tens of seconds, with processing cost growing quadratically with the number of children. No user interaction is required, and confidentiality and integrity are not affected.
The vulnerability exists due to repeated front removal from a Python list in the default right-factor branch of nltk.tree.transforms.chomsky_normal_form in nltk/tree/transforms.py. The loop executes curNode[0:] = [nodeCopy.pop(0), newNode], shifting all remaining list elements on each removal. For a node with N children, N-2 such removals produce O(N^2) work. A remote attacker can submit a bracketed tree with an excessive number of children to an application that parses untrusted input with Tree.fromstring and normalizes it with Tree.chomsky_normal_form() using right factoring. Neither the parser nor the transform limits the child count.
Tree.chomsky_normal_form() delegates to the transform with factor="right" by default. The left-factor branch instead uses nodeCopy.pop(), which removes the last element in constant time and yields linear overall processing; the shared node-copying and bounded head-string operations do not explain the quadratic behavior. The regular-expression wall-clock timeout covers regex matching only, not this pure-Python list loop. The advisory reports that roughly 1.5 MB of treebank input can consume about 14 seconds on one CPU core.
6) Inefficient Algorithmic Complexity (CVE-ID: N/A)
CWE-ID: CWE-407 - Inefficient Algorithmic Complexity
CVSSv4: 8.7 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N]
The vulnerability allows an unauthenticated remote attacker to cause CPU exhaustion and denial of service in applications that parse attacker-influenced CCG lexicon text using the Natural Language Toolkit (NLTK). A few-megabyte input can occupy a single parsing thread for tens of seconds, and concurrent requests can exhaust request workers. No user interaction is required, and confidentiality and integrity are not affected.
The vulnerability exists due to two quadratic-time string-processing loops in nltk/ccg/lexicon.py. In matchBrackets, consuming a wide, flat bracket body repeatedly executes rest = rest[1:], copying the remaining string for each character. Separately, augParseCategory processes long operator chains by repeatedly copying the regular-expression match tail through rest = app[3]. A remote attacker can submit lexicon text containing either a wide bracket body or a long operator chain to an application that exposes parsing of untrusted lexicons, causing sustained CPU consumption. Neither line length nor category width is bounded before these loops execute, and a downstream parse error does not prevent the preceding CPU expenditure.
The public entry point is nltk.ccg.lexicon.fromstring, also exported as nltk.ccg.chart.fromstring; it splits lexicon text into lines and routes categories through nextCategory to the affected processing loops. The recursion-depth guard, MAX_PARSE_DEPTH = 500, limits deeply nested categories but does not constrain these flat, width-based inputs. NLTK's regular-expression wall-clock timeout covers regex matching rather than the pure-Python slicing loops, and its regex source-length limit constrains patterns rather than lexicon text, so neither prevents this resource exhaustion.
7) Insecure DLL loading (CVE-ID: N/A)
CWE-ID: CWE-427 - Uncontrolled Search Path Element
CVSSv4: 8.5 [CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N]
The vulnerability allows a local, unprivileged attacker on a POSIX host to execute arbitrary code as a victim using Natural Language Toolkit (NLTK). Execution occurs when the victim renders a DependencyGraph, including automatic notebook display through _repr_svg_, or calls nltk.parse.dependencygraph.dot2img directly. This provides a local privilege-escalation and lateral-movement primitive through execution in the victim's account. No control over NLTK inputs is required.
The vulnerability exists due to dot2img resolving the Graphviz dot executable by bare name with find_binary and executing the resulting absolute path through subprocess.run without using nltk.pathsec.spawn_trusted. A local, unprivileged attacker can plant a malicious dot executable in a group- or world-writable directory on the victim's PATH, provided that executable is selected before a legitimate installation, or replace an existing dot in such a directory. The lookup rejects relative current-working-directory matches but accepts absolute matches from writable search-path directories. The subsequent execution omits checks that the executable and its parent directories cannot be replaced by another unprivileged user.
Both the text-format and binary-format execution branches in nltk/parse/dependencygraph.py have the same gap; this is not limited to a particular output format or dependent on resolving dot from the current working directory. The subprocess also inherits the environment without the loader and interpreter environment scrubbing normally provided by spawn_trusted. Consequently, variables such as LD_PRELOAD, DYLD_*, and GCONV_PATH reach the child process, creating an additional code-execution surface when an attacker can influence that environment.
8) Inefficient Algorithmic Complexity (CVE-ID: N/A)
CWE-ID: CWE-407 - Inefficient Algorithmic Complexity
CVSSv4: 6.9 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N]
The vulnerability allows an unauthenticated remote attacker to exhaust a CPU core and degrade availability in a network-reachable service that passes untrusted text to NLTK's TreebankWordTokenizer.span_tokenize. A single request containing a sufficiently long run of double-quote characters can cause prolonged processing, resulting in denial of service or increased latency. Concurrent requests could plausibly exhaust a worker pool, although this consequence was not demonstrated against a live deployment. There is no confidentiality or integrity impact.
The vulnerability exists due to an unguarded quadratic-time quote-restoration loop in nltk/tokenize/treebank.py. When input contains double quotes or repeated single quotes, the method constructs a matched list containing quote occurrences and reconstructs quote tokens by repeatedly calling matched.pop(0). Each removal shifts the remaining list elements and takes linear time. An all-double-quote input produces one quote token and one list entry per character, causing cumulative O(n^2) CPU work. A remote attacker can submit a long run of double quotes to a service exposing this span-tokenization API without authentication, special configuration, or user interaction.
Neither span_tokenize nor its underlying tokenize method imposes an input-length cap. The regular-expression timeout backstop in nltk/redos.py does not cover the pure-Python list-removal loop, so it does not bound this processing cost. The neighboring align_tokens call is linear and is not the source of quadratic behavior; memory usage also remains linear. Measurements reported approximately 1.76 seconds for 128,000 double quotes and 6.57 seconds for 256,000 on a single core, while an equal-token-count non-quote control avoided the vulnerable branch and processed substantially faster. No corpus download is required to reach the affected functionality.
9) Inefficient Algorithmic Complexity (CVE-ID: N/A)
CWE-ID: CWE-407 - Inefficient Algorithmic Complexity
CVSSv4: 6.9 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N]
The vulnerability allows an unauthenticated remote attacker to cause quadratic CPU consumption and denial of service or increased latency in a service that passes attacker-controlled text to NLTK's LegalitySyllableTokenizer.tokenize. A single sufficiently long all-vowel token can occupy one CPU core for a prolonged period. Concurrent requests could exhaust available workers, although this consequence was inferred rather than demonstrated against a live deployment. There is no confidentiality or integrity impact.
The vulnerability exists due to an unguarded reverse traversal that repeatedly constructs and hashes a growing string without bounding token length. A remote attacker can submit a long all-vowel token to a network-reachable service that invokes this tokenizer, without authentication, special configuration, or user interaction. After the first vowel, the loop evaluates char_lower + current_onset[::-1] in self.legal_onsets. Because LegalitySyllableTokenizer.onset returns only the prefix before the first vowel, the legal-onset set is vowel-free. For an all-vowel token, the membership branch therefore never matches, the onset flag never becomes true, and the reset branch never clears current_onset. The accumulated string grows on every iteration, and repeated reversal and hashing cost O(k) at iteration k, producing O(n²) total CPU work.
The affected implementation is in nltk/tokenize/legality_principle.py and is publicly exposed through nltk.tokenize. Exploitation requires the application to invoke this syllabification API on untrusted tokens; the default word-tokenization path does not call this class. The behavior depends on attacker-controlled token length and character content, not on a large or particular onset corpus, and is reproducible with a small onset source without downloading a corpus. Memory consumption remains linear while CPU consumption grows quadratically.
10) Link following (CVE-ID: N/A)
CWE-ID: CWE-59 - Improper Link Resolution Before File Access ('Link Following')
CVSSv4: 6.9 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N]
The vulnerability allows an attacker with write access to a trusted NLTK data root to bypass the data-root sandbox and obtain query results from SQLite databases outside all registered roots. The exposed databases must be readable by the application process. If the caller supplies a write query, that query is also redirected to the outside database, and SQLite may create journal or write-ahead-log sidecar files beside it; these are plausible integrity effects, not a demonstrated arbitrary-write primitive.
The vulnerability exists due to nltk.sem.chat80.sql_query() passing the path returned by nltk.data.find(dbname) directly to sqlite3.connect() without validating its resolved location. Resource-name checks are lexical, while FileSystemPathPointer uses os.path.abspath and an existence check without resolving symbolic links. A remote attacker can exploit this behavior only if they also have write capability within a registered data root, allowing them to plant a symbolic link at the expected database resource location and have the application query that resource through sql_query() or sql_demo(). SQLite follows the link to the attacker-selected outside database even though the resource name itself passes the lexical checks.
The sandbox normally enforces containment through nltk.pathsec.open and validate_path, which resolve real paths and reject targets outside the allowed roots. FileSystemPathPointer.open() reaches this enforcement, but the direct SQLite connection bypasses it. An in-root link such as corpora/city_database/city.db can therefore be rejected by the pointer's sandboxed open operation while still yielding outside database rows through sql_query(). Non-SQLite targets raise sqlite3.DatabaseError rather than exposing arbitrary file contents. Exploitation requires the stated local filesystem write capability; network access is not inherently required.
11) Resource exhaustion (CVE-ID: N/A)
CWE-ID: CWE-400 - Resource exhaustion
CVSSv4: 8.7 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N]
The vulnerability allows an attacker to cause unbounded CPU consumption and denial of service in Natural Language Toolkit applications that invoke SteppingRecursiveDescentParser.parse() on attacker-influenced grammars and token sequences. A single crafted parse can monopolize a CPU core indefinitely, and repeated requests can exhaust a bounded worker pool, preventing legitimate users from receiving service.
The vulnerability exists due to an unbounded top-down search over ambiguous or self-embedding context-free grammars without a wall-clock or step limit. A remote attacker can influence the grammar and supply a short token sequence to an exposed application that invokes this parser, causing it to enumerate an exponentially growing derivation space. The subclass overrides parse() and _parse(): parse() initializes the parser and repeatedly calls step(), while the _parse() override captures state and returns a sentinel instead of entering the guarded base implementation. Consequently, this path neither arms nor checks the inherited deadline or time limit, and the constructor exposes no max_time parameter. The search has no effective memoization: its tried-expansion and tried-match records deduplicate only identical productions at identical full partial-tree states, leaving the combinatorial enumeration intact.
The affected component is nltk.parse.recursivedescent.SteppingRecursiveDescentParser in nltk/parse/recursivedescent.py, also publicly exported through nltk.parse. An example grammar is S → 'a' S S | 'a', paired with 17 copies of the token 'a'; its derivation space grows on the order of the Catalan numbers. The stepping loop uses an explicit history stack, so this failure is CPU exhaustion rather than an early RecursionError. Exploitation requires an application to use this particular parser with an attacker-influenced grammar and input; its primary use for debugging, interactive stepping, and a GUI narrows practical exposure. In an exposed deployment accepting such requests, no authentication, privileges, or user interaction is required.
12) Link following (CVE-ID: N/A)
CWE-ID: CWE-59 - Improper Link Resolution Before File Access ('Link Following')
CVSSv4: 8.7 [CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N]
The vulnerability allows an attacker who controls a YCOE corpus loaded by a victim application to disclose file content outside that corpus root but inside an allowed NLTK data sandbox. Natural Language Toolkit's YCOECorpusReader can return this content through parsed_sents(), tagged_words(), tagged_sents(), words(), and related methods despite pathsec.ENFORCE=True. In shared-data or automated-ingestion deployments, exposed content can include other corpora and sensitive temporary files in the shared system temporary directory, which is a default allowed root. The demonstrated impact is read-only cross-corpus disclosure, with no integrity or availability impact.
The vulnerability exists due to lexical-only path containment checks combined with missing parent-root validation when constructing nested corpus readers. A remote attacker can supply an attacker-controlled YCOE corpus containing a directory symlink named psd or pos that points outside the corpus root but inside an allowed data root, provided the victim application loads that corpus. YCOECorpusReader constructs nested readers using self.root.join(), while FileSystemPathPointer.join() checks only a normalized lexical prefix and does not resolve symlinks; FileSystemPathPointer initialization likewise uses abspath without resolving links. CorpusReader.__init__ then validates the nested root without required_root, so only the global sandbox check runs and accepts the resolved in-sandbox target instead of enforcing containment within the enclosing YCOE corpus.
Subsequent read-time validation uses the nested reader's already-escaped root as required_root, and find_corpus_fileids() also anchors its resolved-root containment check to that target, so neither restores the original corpus boundary. The affected construction and validation logic resides in nltk/corpus/reader/ycoe.py, nltk/data.py, and nltk/corpus/reader/api.py. Ordinary non-symlink psd and pos directories remain contained, and symlinks resolving outside every allowed root are rejected during construction. This is therefore a per-corpus containment bypass within the global data sandbox, not an arbitrary-filesystem read.
Remediation
Install update from vendor's website.
References
- https://github.com/nltk/nltk/security/advisories/GHSA-j456-xh4h-cpf2
- https://github.com/nltk/nltk/security/advisories/GHSA-fg9w-wfp5-j933
- https://github.com/nltk/nltk/security/advisories/GHSA-59f9-gqg8-mqpj
- https://github.com/nltk/nltk/security/advisories/GHSA-v8f3-6phw-6mh9
- https://github.com/nltk/nltk/security/advisories/GHSA-r53h-rw34-8h97
- https://github.com/nltk/nltk/security/advisories/GHSA-89p3-fcch-88ph
- https://github.com/nltk/nltk/security/advisories/GHSA-7mxv-7h3q-9324
- https://github.com/nltk/nltk/security/advisories/GHSA-ffr9-mgrr-wcvr
- https://github.com/nltk/nltk/security/advisories/GHSA-gpwc-27cw-rh9r
- https://github.com/nltk/nltk/security/advisories/GHSA-xmfg-f9cm-w86q
- https://github.com/nltk/nltk/security/advisories/GHSA-8fx7-8jr8-84rv
- https://github.com/nltk/nltk/security/advisories/GHSA-qg35-44qx-7vrr