Skip to content
NLTKGHSA-cw6x-m8jw-qmrh

NLTK: Uncontrolled recursion in nltk.featstruct.FeatStructReader causes unhandled RecursionError (DoS) via deeply nested feature-structure input

Medium5.3CVE-2026-81724 · Published Sep 2, 2026 · updated Sep 10, 2026

GitHub advisory

Affected versions

PackageAffectedFixed in
nltk
PyPI
< 3.10.33.10.3
Details and references

### Summary `nltk.featstruct.FeatStructReader` (used by `FeatStruct(str)` and by `FeatureGrammar.fromstring()`) parses feature-structure strings such as `[a=1]` with a recursive-descent parser that has no nesting-depth limit. A small, trivially-crafted input (~700 bytes) with deeply nested brackets drives the parser past Python's recursion limit and raises an **unhandled `RecursionError`** instead of the library's normal, catchable `ValueError`/`LogicalExpressionException`. Any application that parses user-supplied feature-structure or feature-grammar text (e.g. NLP teaching tools, grammar "playgrounds", unification-grammar-based NLU pipelines) can be crashed by an unauthenticated input with no special privileges. This is a Denial of Service issue (CWE-674, Uncontrolled Recursion), not a memory-safety or code-execution issue. This appears to be the same bug class as two issues already fixed elsewhere in the codebase , `nltk/jsontags.py` (`JSONTaggedDecoder.decode_obj`, guarded by `MAX_DECODE_DEPTH = 200`) and `nltk/sem/logic.py` (`LogicParser`, guarded by `MAX_PARSE_DEPTH = 200`) , but `nltk/featstruct.py` does not have an equivalent guard. ### Details The recursive call chain (current `develop` branch, `nltk/featstruct.py`): 1. `FeatStructReader.fromstring()` ([`featstruct.py:2184`](nltk/featstruct.py#L2184)) calls `read_partial()` → `_read_partial()` ([`featstruct.py:2250`](nltk/featstruct.py#L2250)). 2. `_read_partial()` dispatches to `_read_partial_featdict()`, which calls `_read_value()` ([`featstruct.py:2436`](nltk/featstruct.py#L2436)) for each feature's value. 3. `_read_value()` calls `read_value()` ([`featstruct.py:2442`](nltk/featstruct.py#L2442)), which matches the value against `VALUE_HANDLERS` ([`featstruct.py:2478`](nltk/featstruct.py#L2478)). 4. If the value itself starts with `[` (a nested feature structure), the matched handler is `read_fstruct_value` ([`featstruct.py:2479`](nltk/featstruct.py#L2479), defined at [`featstruct.py:2495`](nltk/featstruct.py#L2495)): ```python def read_fstruct_value(self, s, position, reentrances, match): return self.read_partial(s, position, reentrances) ``` This calls `read_partial()` again, which re-enters `_read_partial()` , the same function from step 1. This closes a recursive cycle (`_read_partial → _read_value → read_value → read_fstruct_value → read_partial → _read_partial → ...`) with **no depth counter, no `MAX_*_DEPTH` constant, and no `try/except RecursionError`** anywhere in the class. Each additional `[` in the input adds one more full cycle of Python stack frames. Once the input nests deeply enough, Python's own recursion-limit protection fires and raises `RecursionError`, which is not a subclass of `ValueError` (the exception type this parser's own `_error()` helper raises for normal, well-formed parse errors) and therefore propagates uncaught through this API. For comparison, `nltk/sem/logic.py`'s `LogicParser` was hardened against exactly this class of issue: ```python #: Maximum expression-nesting depth the recursive-descent parser will #: descend to. Deeply nested input would otherwise recurse until Python #: raises an uncaught RecursionError and crashes the caller #: (uncontrolled recursion, CWE-674); past this depth a normal #: LogicalExpressionException is raised instead. Configurable. MAX_PARSE_DEPTH = 200 ``` (`nltk/sem/logic.py:102-107`), and `nltk/jsontags.py`'s `JSONTaggedDecoder` similarly has `MAX_DECODE_DEPTH = 200` with an explicit depth check. `nltk/featstruct.py` has no analogous protection. `FeatureGrammar.fromstring()` (`nltk/grammar.py`) parses feature structures embedded in FCFG grammar rules via the same `FeatStructReader`, so the same crash is reachable through grammar-string parsing as well as through `FeatStruct()` directly. ### PoC Verified against the current `develop` branch in a clean virtualenv (Python 3.12, NLTK installed from this checkout via `pip install -e .`): ```python from nltk.featstruct impor

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Severity from
GitHub (reviewed advisory)
Weakness
CWE-674
Also known as
CVE-2026-81724, PYSEC-2026-3739

More NLTK advisories

All NLTK
DateAdvisory
Sep 1NLTK: JVM argument injection bypass via per-call options in the NLTK Stanford wrappers (incomplete fix of CVE-2026-12841)
CVE-2026-79675Critical9.8fixed in 3.10.3
Sep 1NLTK: Uncontrolled search path when invoking the Graphviz 'dot' binary
CVE-2026-78680High7.8fixed in 3.10.3
Sep 2NLTK: Uncontrolled resource consumption in RecursiveDescentParser via ambiguous or left-recursive grammars
CVE-2026-12876Mediumfixed in 3.10.3
Sep 2NLTK: Quadratic CPU Exhaustion in `XMLCorpusView._read_xml_fragment()`
CVE-2026-81723Medium3.7fixed in 3.10.3
Sep 2NLTK: Model-artifact APIs bypass pathsec and touch files outside allowed roots
CVE-2026-81726High7.0no fix yet
Sep 2NLTK: Downloader.download follows hardlinks and overwrites outside-root files
CVE-2026-81727Medium7.1fixed in 3.10.3

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.