CVE-2026-81724: NLTK Denial of Service Vulnerability

MODERATE
Advisory Identifier: GHSA-cw6x-m8jw-qmrh
CVSS 5.3
CWE-674
Published
Advisory Summary

CVE-2026-81724 is a MODERATE (CVSS 5.3) denial of service vulnerability (CWE-674: Uncontrolled Recursion) affecting NLTK libraries in the PyPI ecosystem. The issue involves uncontrolled recursion in nltk.featstruct.featstructreader causes unhandled recursionerror (dos) via deeply nested feature-structure input. Remediation is available by upgrading affected packages to version 3.10.3.

CVE-2026-81724 at a Glance

CVE Identifier CVE-2026-81724
GHSA Identifier GHSA-cw6x-m8jw-qmrh
Severity MODERATE
CVSS v3.1 Base Score 5.3
CWE Classification CWE-674: Uncontrolled Recursion
Affected Product NLTK
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 5.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability Low (A:L)

How to Fix CVE-2026-81724

To remediate this vulnerability, upgrade all affected NLTK dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

nltk fixed: 3.10.3
pip install nltk>=3.10.3
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show nltk

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-81724, inspect your installed packages using pip:

pip show nltk

Analyze the terminal output to locate any version of NLTK in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.10.3), your application is affected.

Note: The pip show nltk command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-674: Uncontrolled Recursion, where the product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack. In NLTK, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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 import FeatStruct depth = 167 payload = "[a=" * depth + "1" + "]" * depth # 669 bytes FeatStruct(payload) ``` Result: ``` Traceback (most recent call last): ... File ".../nltk/featstruct.py", line 2310, in _read_partial_featdict value, position = self._read_value(name, s, position, reentrances) File ".../nltk/featstruct.py", line 2440, in _read_value return self.read_value(s, position, reentrances) File ".../nltk/featstruct.py", line 2446, in read_value return handler_func(s, position, reentrances, match) [... repeats ~167 times ...] RecursionError: maximum recursion depth exceeded ``` - Crash threshold: nesting depth 167 (binary-searched between 50 and 200). - Payload size: 669 bytes — fits trivially in a single HTTP request body/query parameter. - Time to crash: <2ms — no resource exhaustion is needed, only recursion depth. Minimal reproduction (no server required): ```bash python3 -c " from nltk.featstruct import FeatStruct FeatStruct('[a=' * 200 + '1' + ']' * 200) " ``` Illustrative server-side context (not part of NLTK itself, but representative of how the bug becomes reachable): ```python from flask import Flask, request from nltk.featstruct import FeatStruct app = Flask(__name__) @app.route("/parse", methods=["POST"]) def parse_grammar(): return {"result": str(FeatStruct(request.json["grammar"]))} ``` A POST of `{"grammar": "[a=" * 200 + "1" + "]" * 200}` to this endpoint raises the uncaught `RecursionError` inside the request handler. ### Impact **Vulnerability type:** Denial of Service via uncontrolled recursion (CWE-674). This is not a memory-corruption bug and does not lead to code execution or data disclosure — Python's own recursion-limit safety net converts what would be a C-level stack overflow into a catchable (but here, uncaught) `RecursionError`. **Who is affected:** Any application that passes externally-supplied text into `nltk.featstruct.FeatStruct()` or `nltk.grammar.FeatureGrammar.fromstring()` — for example, NLP/computational-linguistics teaching tools, unification-grammar demo services, or NLU pipelines that accept user-authored feature grammars. This is a narrower slice of NLTK's user base than, e.g., tokenization or POS tagging, since feature-structure/unification-grammar parsing is a more specialized part of the library. **Practical severity depends on deployment:** - In typical WSGI-style web frameworks (Flask/Django/FastAPI behind gunicorn/uwsgi), an uncaught exception inside a request handler is caught at the framework/server boundary: the single request fails (HTTP 500), the worker process itself survives, and unaffected requests are unimpacted. - In single-threaded or per-task-unprotected contexts (e.g. a queue-consuming worker without per-task exception isolation), the uncaught `RecursionError` can terminate the entire process; without a process supervisor that auto-restarts it, this is a persistent outage until manually restarted. An attacker who repeats the payload can keep such a worker in a crash loop for as long as the attack continues. **Suggested fix:** Add a depth counter and a `MAX_PARSE_DEPTH`-style constant to `FeatStructReader`, mirroring the existing fix in `nltk/sem/logic.py`, and raise the library's normal `ValueError`-based parse error once the limit is exceeded instead of letting `RecursionError` propagate.

CVE-2026-81724 Affected NLTK Versions

1 component(s)
Fixed Versions:
3.10.3
Product / Package Ecosystem Affected Versions Fixed Version
nltk
PyPI
< 3.10.3
3.10.3

References

Frequently Asked Questions

What is CVE-2026-81724?
CVE-2026-81724 is a security vulnerability in NLTK involving Uncontrolled recursion in nltk.featstruct.FeatStructReader causes unhandled RecursionError (DoS) via deeply nested feature-structure input under CWE-674: Uncontrolled Recursion.
Which NLTK versions are affected?
Affected versions include releases prior to 3.10.3 across affected packages: nltk.
What version fixes CVE-2026-81724?
Upgrading to 3.10.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is nltk affected?
Yes, nltk is affected in releases prior to 3.10.3. Upgrade your dependency to 3.10.3 or later.
What is the CVSS score of CVE-2026-81724?
CVE-2026-81724 has a CVSS v3.1 base score of 5.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L.
What CWE is associated with CVE-2026-81724?
CVE-2026-81724 is classified under CWE-674: Uncontrolled Recursion (The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack.).
How can I check PyPI dependencies for CVE-2026-81724?
Run the dependency tree command: 'pip show nltk'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-cw6x-m8jw-qmrh",
  "modified" : "2026-09-02T14:33:22Z",
  "published" : "2026-09-02T14:33:22Z",
  "aliases" : [ "CVE-2026-81724" ],
  "summary" : "NLTK: Uncontrolled recursion in nltk.featstruct.FeatStructReader causes unhandled RecursionError (DoS) via deeply nested feature-structure input",
  "details" : "### Summary\n\n`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.\n\nThis 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.\n\n### Details\n\nThe recursive call chain (current `develop` branch, `nltk/featstruct.py`):\n\n1. `FeatStructReader.fromstring()` ([`featstruct.py:2184`](nltk/featstruct.py#L2184)) calls `read_partial()` → `_read_partial()` ([`featstruct.py:2250`](nltk/featstruct.py#L2250)).\n2. `_read_partial()` dispatches to `_read_partial_featdict()`, which calls `_read_value()` ([`featstruct.py:2436`](nltk/featstruct.py#L2436)) for each feature's value.\n3. `_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)).\n4. 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)):\n   ```python\n   def read_fstruct_value(self, s, position, reentrances, match):\n       return self.read_partial(s, position, reentrances)\n   ```\n   This calls `read_partial()` again, which re-enters `_read_partial()` — the same function from step 1.\n\nThis 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.\n\nFor comparison, `nltk/sem/logic.py`'s `LogicParser` was hardened against exactly this class of issue:\n```python\n#: Maximum expression-nesting depth the recursive-descent parser will\n#: descend to. Deeply nested input would otherwise recurse until Python\n#: raises an uncaught RecursionError and crashes the caller\n#: (uncontrolled recursion, CWE-674); past this depth a normal\n#: LogicalExpressionException is raised instead. Configurable.\nMAX_PARSE_DEPTH = 200\n```\n(`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.\n\n`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.\n\n### PoC\n\nVerified against the current `develop` branch in a clean virtualenv (Python 3.12, NLTK installed from this checkout via `pip install -e .`):\n\n```python\nfrom nltk.featstruct import FeatStruct\n\ndepth = 167\npayload = \"[a=\" * depth + \"1\" + \"]\" * depth   # 669 bytes\nFeatStruct(payload)\n```\n\nResult:\n```\nTraceback (most recent call last):\n  ...\n  File \".../nltk/featstruct.py\", line 2310, in _read_partial_featdict\n    value, position = self._read_value(name, s, position, reentrances)\n  File \".../nltk/featstruct.py\", line 2440, in _read_value\n    return self.read_value(s, position, reentrances)\n  File \".../nltk/featstruct.py\", line 2446, in read_value\n    return handler_func(s, position, reentrances, match)\n  [... repeats ~167 times ...]\nRecursionError: maximum recursion depth exceeded\n```\n\n- Crash threshold: nesting depth 167 (binary-searched between 50 and 200).\n- Payload size: 669 bytes — fits trivially in a single HTTP request body/query parameter.\n- Time to crash: <2ms — no resource exhaustion is needed, only recursion depth.\n\nMinimal reproduction (no server required):\n```bash\npython3 -c \"\nfrom nltk.featstruct import FeatStruct\nFeatStruct('[a=' * 200 + '1' + ']' * 200)\n\"\n```\n\nIllustrative server-side context (not part of NLTK itself, but representative of how the bug becomes reachable):\n```python\nfrom flask import Flask, request\nfrom nltk.featstruct import FeatStruct\n\napp = Flask(__name__)\n\n@app.route(\"/parse\", methods=[\"POST\"])\ndef parse_grammar():\n    return {\"result\": str(FeatStruct(request.json[\"grammar\"]))}\n```\nA POST of `{\"grammar\": \"[a=\" * 200 + \"1\" + \"]\" * 200}` to this endpoint raises the uncaught `RecursionError` inside the request handler.\n\n### Impact\n\n**Vulnerability type:** Denial of Service via uncontrolled recursion (CWE-674). This is not a memory-corruption bug and does not lead to code execution or data disclosure — Python's own recursion-limit safety net converts what would be a C-level stack overflow into a catchable (but here, uncaught) `RecursionError`.\n\n**Who is affected:** Any application that passes externally-supplied text into `nltk.featstruct.FeatStruct()` or `nltk.grammar.FeatureGrammar.fromstring()` — for example, NLP/computational-linguistics teaching tools, unification-grammar demo services, or NLU pipelines that accept user-authored feature grammars. This is a narrower slice of NLTK's user base than, e.g., tokenization or POS tagging, since feature-structure/unification-grammar parsing is a more specialized part of the library.\n\n**Practical severity depends on deployment:**\n- In typical WSGI-style web frameworks (Flask/Django/FastAPI behind gunicorn/uwsgi), an uncaught exception inside a request handler is caught at the framework/server boundary: the single request fails (HTTP 500), the worker process itself survives, and unaffected requests are unimpacted.\n- In single-threaded or per-task-unprotected contexts (e.g. a queue-consuming worker without per-task exception isolation), the uncaught `RecursionError` can terminate the entire process; without a process supervisor that auto-restarts it, this is a persistent outage until manually restarted. An attacker who repeats the payload can keep such a worker in a crash loop for as long as the attack continues.\n\n**Suggested fix:** Add a depth counter and a `MAX_PARSE_DEPTH`-style constant to `FeatStructReader`, mirroring the existing fix in `nltk/sem/logic.py`, and raise the library's normal `ValueError`-based parse error once the limit is exceeded instead of letting `RecursionError` propagate.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L"
  }, {
    "type" : "CVSS_V4",
    "score" : "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"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "nltk"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.10.3"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 3.10.2"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/nltk/nltk/security/advisories/GHSA-cw6x-m8jw-qmrh"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-81724"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nltk/nltk/commit/43c7b78cc8ea37e5cd3a129e27e32c415ea21cf1"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/nltk/nltk"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nltk/nltk/releases/tag/v3.10.3"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3739.yaml"
  }, {
    "type" : "WEB",
    "url" : "https://www.vulncheck.com/advisories/nltk-before-3.10.3-denial-of-service-via-uncontrolled-recursion"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-674" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-02T14:33:22Z",
    "nvd_published_at" : null
  }
}