CVE-2026-126298: NLTK: Quadratic-time CPU denial of service in Tree.chomsky_normal_form right-factor binarization via list.pop(0)

Published Nov 13, 2026
·
Updated

Summary

Tree.chomskynormalform() binarizes a parse tree and defaults to right factoring. For a node with N children the right-factor branch repeatedly removes the first element of a working list with list.pop(0), which shifts all remaining elements each time, so binarizing one wide node costs O(N^2). An application that converts an attacker-supplied treebank tree to Chomsky Normal Form can be forced into tens of seconds of single-threaded CPU by one wide flat tree. No authentication or user interaction is required.

Root Cause

In the default right-factor branch the transform executes curNode[0:] = [nodeCopy.pop(0), newNode] inside a loop over the N children of a node. list.pop(0) is O(len) because it shifts every remaining element down by one; performed N-2 times over a shrinking length-N list this sums to O(N^2). The left-factor branch instead uses nodeCopy.pop() (removing the last element), which is O(1) and linear overall, so the two branches differ only by this front-versus-back removal. No cap is placed on the child count of a node, and the public Tree.fromstring parser accepts an arbitrarily wide bracketed tree.

text Affected function: - nltk.tree.transforms.chomskynormalform (nltk/tree/transforms.py; right-factor sink at line 168, O(1) left control at line 179) Public entry points: nltk.tree.Tree.chomskynormalform (nltk/tree/tree.py:455, default factor="right"); nltk.tree.Tree.fromstring (nltk/tree/tree.py:592).

Impact

An attacker who can influence the tree that an application normalizes to Chomsky Normal Form causes sustained single-threaded CPU consumption (availability loss). The path is network-reachable (AV:N) and needs no privileges or user interaction (PR:N, UI:N). Because the cost grows quadratically with the attacker-chosen child count, roughly 1.5 MB of treebank input costs about 14 seconds on one core and scales as N^2 above that. Confidentiality and integrity are not affected. The NLTK regular-expression wall-clock timeout wraps regex matching only and does not wrap this pure-Python list loop.

Proof of Concept

text Environment: nltk 3.10.3, CPython. Drive the public transform on a wide flat tree.

python import time from nltk.tree import Tree

for N in (80000, 160000, 320000): s = "(S " + "(A a)" N + ")" t = Tree.fromstring(s) start = time.perfcounter() t.chomskynormalform() # default factor="right" print("right", N, round(time.perfcounter() - start, 3), "s")

text right (default): N=80000 1.972s | N=160000 5.004s (2.54x) | N=320000 14.518s (2.90x) -> super-linear left (control): N=80000 1.399s | N=160000 2.731s (1.95x) | N=320000 5.379s (1.97x) -> linear

Attack Chain

1. Exposure: An application parses an attacker-influenced treebank string with the public Tree.fromstring and normalizes it with Tree.chomskynormalform(). 2. Control: The attacker controls the number of children of a node (a wide flat tree such as (S (A a)(A a)...(A a))); no child-count cap exists in fromstring or the transform. 3. Path: Tree.chomskynormalform delegates to nltk.tree.transforms.chomskynormalform with the default factor="right". 4. Guard: None. There is no width bound on the loop, and the regex match timeout does not wrap this pure-Python list operation. 5. Primitive: curNode[0:] = [nodeCopy.pop(0), newNode] performs an O(N) front shift on each of N iterations. 6. Result: O(N^2) single-core CPU exhaustion; roughly 1.5 MB of input costs about 14 seconds and scales quadratically, blocking the processing thread.

Bypass Evidence

There is no guard to bypass on this path: neither Tree.fromstring nor chomskynormalform caps the child count, and the regular-expression wall-clock timeout wraps only regex matching, not this list loop. The finding was confirmed by fresh timing that isolates the cause: the right-factor default and the left-factor branch build the identical bounded head strings and copy the node once, so the only differing term is pop(0) versus pop(). The measured right-versus-left divergence grows with N (right rises toward a 4x per-doubling ratio while left holds near 2x), which proves a quadratic component beyond the shared linear costs. A relevant corroboration is that the inverse transform unchomskynormalform was already rewritten to a single linear pass to remove exactly this quadratic class (see the comment at nltk/tree/transforms.py:195-201), while the forward default path retained the pop(0) idiom. The strongest disproof attempt (that the head-string join dominates and is linear) fails because that join is bounded and present in both branches and therefore cannot explain the branch-dependent quadratic growth.

Affected Versions

- Ecosystem: PyPI (pip) - Package: nltk - Confirmed affected range: <= 3.10.3 - Latest release checked: 3.10.3 (PyPI, https://pypi.org/pypi/nltk/json) - Fix status: 3.10.4

The right-factor pop(0) idiom ships in every release carrying this transform and is present in the current latest published release, 3.10.3. The lower boundary was not bisected, so the introducing version is left open.

Suggested Fix

Rewrite the right-factor binarization to avoid removing from the front of a list: iterate the children with an index cursor or a collections.deque so each step is constant time, mirroring the linear rebuild already applied to unchomskynormalform. As defense in depth, bound the child count accepted for normalization and raise a clear error past a configurable maximum. No configuration-only mitigation removes the exposure for a service that normalizes untrusted trees; until a fix ships, callers should cap accepted tree width and run the transform under a CPU or time budget in an isolated worker.

Reported by zx (GitHub: @manus-pi).

Fix status (2026-09-28)

PR: #3909 "Fix quadratic DoS in Tree.chomskynormalform right-factor binarization (GHSA-r53h-rw34-8h97)", merged 2026-09-23, merge commit 2f53e20ce on develop.

Fix commits: 457ebe349 (linear front consume in chomskynormalform) and 677857dca (refuse a binarisation that would nest a tree beyond MAXTREEDEPTH). Follow-up #3917 (merge 9ba132e43, commit f0686dea6) walks Tree equality and conversion with an explicit stack so the deep-tree tests pass on Python 3.10 and 3.11.

Probe: nltk/test/unit/securityprobes/ghsar53hrw348h97.py; verdict on develop 2fa3f48fe: FIXED (chomskynormalform scales 5.1x over a 4x input).

Other sources

NLTK before 3.10.4 contains a denial of service vulnerability in Tree.chomskynormalform that allows remote attackers to exhaust CPU through quadratic-time right-factor binarization using list.pop(0). Attackers can supply a wide flat treebank tree parsed by Tree.fromstring to force tens of seconds of single-threaded CPU consumption, blocking the processing thread.

— MITRE

Affected Software

3 affected componentsFixes available
pypi/nltk<3.10.4
pip/nltk<=3.10.3
3.10.4
pip/nltk<3.10.4
3.10.4

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/nltk to a version that resolves this vulnerability.

    Fixed in 3.10.4
  2. Upgrade

    Upgrade nltk to a version that resolves this vulnerability.

    Fixed in 3.10.4
  3. Compensating control

    Before normalizing untrusted trees, cap the accepted child count and reject trees exceeding the configured maximum.

  4. Compensating control

    Until the fix is deployed, run normalization in an isolated worker under a CPU or time budget.

Event History

Nov 13, 2026
CVE Published
via MITRE·09:36 AM
Data Sourced
via MITRE·09:36 AM
DescriptionSeverityWeakness
Data Sourced
via NVD·10:17 AM
DescriptionSeverityWeakness
Nov 14, 2026
Advisory Published
via GitHub·12:07 AM
Data Sourced
via GitHub·12:07 AM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed to remote exploitation?

Any application using an affected NLTK version that accepts attacker-controlled tree strings and passes them to Tree.fromstring followed by Tree.chomsky_normal_form is exposed. The attack can be performed remotely without authentication or user interaction when that parsing path is reachable over the network.

2

What input is required to trigger the CPU exhaustion?

An attacker needs to supply a wide, flat treebank tree. During right-factor binarization, the vulnerable implementation uses list.pop(0), resulting in quadratic CPU consumption and potentially blocking the processing thread for tens of seconds.

3

Does this affect confidentiality or integrity?

The provided impact information identifies availability as affected, with no confidentiality or integrity impact. The issue is a single-threaded CPU denial of service.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203