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
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| nltk PyPI | < 3.10.3 | 3.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
- github.com/nltk/nltk/security/advisories/GHSA-cw6x-m8jw-qmrh
- nvd.nist.gov/vuln/detail/CVE-2026-81724
- github.com/nltk/nltk/commit/43c7b78cc8ea37e5cd3a129e27e32c415ea21cf1
- github.com/nltk/nltk
- github.com/nltk/nltk/releases/tag/v3.10.3
- github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3739.yaml
- www.vulncheck.com/advisories/nltk-before-3.10.3-denial-of-service-via-uncontrolled-recursion
More NLTK advisories
All NLTK| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Sep 1 | NLTK: 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 | Critical9.8 | 3.10.3 |
| Sep 1 | NLTK: Uncontrolled search path when invoking the Graphviz 'dot' binary CVE-2026-78680High7.8fixed in 3.10.3 | High7.8 | 3.10.3 |
| Sep 2 | NLTK: Uncontrolled resource consumption in RecursiveDescentParser via ambiguous or left-recursive grammars CVE-2026-12876Mediumfixed in 3.10.3 | Medium | 3.10.3 |
| Sep 2 | NLTK: Quadratic CPU Exhaustion in `XMLCorpusView._read_xml_fragment()` CVE-2026-81723Medium3.7fixed in 3.10.3 | Medium3.7 | 3.10.3 |
| Sep 2 | NLTK: Model-artifact APIs bypass pathsec and touch files outside allowed roots CVE-2026-81726High7.0no fix yet | High7.0 | No fix yet |
| Sep 2 | NLTK: Downloader.download follows hardlinks and overwrites outside-root files CVE-2026-81727Medium7.1fixed in 3.10.3 | Medium7.1 | 3.10.3 |