Media

NLTK: Quadratic CPU Exhaustion in `XMLCorpusView._read_xml_fragment()`

  • Publicada: 02.09.2026
  • Actualizado: 11.10.2026 15:18

Summary

XMLCorpusView._read_xml_fragment() reads a corpus file in 1 KiB blocks, appending each block to a growing fragment string, then calls _VALID_XML_RE.match(fragment) on the full accumulated buffer every iteration. Because each iteration rescans the entire accumulated fragment, the total amount of work grows quadratically with input size.

Commit c9c332284 (CWE-1333) made each match() call linear. The quadratic behavior is separate: the loop calls match() once per 1 KiB block, each time on a longer buffer.

On the test system, an 8 MiB malformed XML file consumed approximately 48 CPU-seconds through the public BNCCorpusReader.words() API with no source modification. Absolute timings vary by hardware. _read_xml_fragment() imposes no limit on fragment size or iteration count.

Details

File: nltk/corpus/reader/xmldocs.py
Function: XMLCorpusView._read_xml_fragment(), lines 261–308

The relevant loop:

python
fragment = ""
while True:
    fragment += stream.read(self._BLOCK_SIZE)      # grows by 1 KiB per iteration
    if self._VALID_XML_RE.match(fragment):         # rescans full buffer each time
        return fragment
    ...
    last_open_bracket = fragment.rfind("<")
    if last_open_bracket > 0:                      # False for single-'<' payload
        if self._VALID_XML_RE.match(fragment[:last_open_bracket]):
            return ...
    # loop continues

For a payload of b'<' + b'a' * (N-1):

  • For this malformed input, _VALID_XML_RE.match(fragment) does not succeed because the unterminated tag prevents the expression from matching before EOF.
  • fragment.rfind("<") returns 0; the guard last_open_bracket > 0 is False, so the backtrack branch is never taken.
  • The only exit is EOF, after all N bytes are consumed.

Affected readers -> readers that rely on XMLCorpusView, including BNCCorpusReader, NPSChatCorpusReader, SemcorCorpusReader, MTECorpusReader, NKJPCorpusReader, FrameNetCorpusReader, VerbNetCorpusReader, and direct XMLCorpusView instantiation. XMLCorpusReader.xml() is not affected -> it calls defusedxml.safe_parse().

PoC

Requires only pip install nltk. No corpus data needed.

python
from pathlib import Path
from tempfile import TemporaryDirectory
from time import perf_counter
from nltk.corpus.reader.bnc import BNCCorpusReader

SIZES_KIB = (256, 512, 1024, 2048, 4096, 8192)
results = []
with TemporaryDirectory() as directory:
    root = Path(directory)
    malformed = root / "unterminated.xml"
    for kib in SIZES_KIB:
        malformed.write_bytes(b"<" + b"a" * (kib * 1024 - 1))
        t = perf_counter()
        try:
            list(BNCCorpusReader(str(root), [malformed.name]).words())
        except ValueError as e:
            assert "tag not closed" in str(e)
        results.append(perf_counter() - t)

print("KiB      seconds   growth")
for i, (kib, elapsed) in enumerate(zip(SIZES_KIB, results)):
    ratio = "-" if i == 0 else f"{elapsed / results[i-1]:.2f}x"
    print(f"{kib:5d}  {elapsed:9.3f}  {ratio}")

Runtime should increase by approximately fourfold for each doubling of input size, although absolute timings vary by hardware.

During verification, _VALID_XML_RE.match() was instrumented to record the size of each input. For a 256 KiB malformed file it was invoked 257 times on monotonically increasing buffers (1024, 2048, …, 262144 bytes), with the final call occurring after EOF. This confirms that every iteration rescans the accumulated fragment.

Impact

Applications that process attacker-controlled XML corpus files through an affected reader are vulnerable. The attacker needs only write access to a path the reader will open. No NLTK credentials or special privileges required. Offline tools reading only trusted local corpora are not at risk.

Affected versions: Verified in NLTK 3.9.4, 3.10.0, and the current develop branch. Historical inspection indicates the same loop structure has existed since the introduction of XMLCorpusView (2007), but only the listed versions were experimentally verified. No patch exists in any published release.

This issue results in CPU exhaustion and may allow denial of service in applications that process attacker-controlled XML corpus files.

Suggested Fix

Avoid rescanning the accumulated fragment from the beginning after each 1 KiB read. Incremental parsing, bounded fragment accumulation, or another streaming approach would eliminate the quadratic behavior while preserving existing semantics.

A regression test should verify that BNCCorpusReader.words() raises ValueError within a fixed timeout (e.g. 5 seconds) against a 2 MiB malformed input. The existing test_xmldocs_security.py covers only the prior ReDoS payloads and does not exercise this path.

Software afectado

Haz clic en una tecnología para ver todas sus vulnerabilidades.

PlataformaPaqueteVersiones vulnerablesCorregida en
Python (PyPI) nltk < 3.10.3 3.10.3

Referencias

Puntuación CVSS 3.7/10
Vector CVSS
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L
Debilidad (CWE)
CWE-400, CWE-407
Plataforma
Python (PyPI)
Paquete
nltk
Publicada
02.09.2026
Actualizado
11.10.2026 15:18

Fuentes y licencias

OSV / GitHub Advisory Database - PYSEC-2026-3871 - PyPA Advisory Database; CC BY 4.0

OSV / GitHub Advisory Database - GHSA-vp2x-qp44-57v7 - CC BY 4.0

Historial de vulnerabilidades: nltk

Todas las vulnerabilidades publicadas de este paquete (Python (PyPI)), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.

Todas (52) en el archivo
Entradas en total
52
Primera → última
08.2019 09.2026
Sin corrección
6
Explotada en ataques
0

Distribución por gravedad

  • Crítica 5
  • Alta 32
  • Media 13
  • Baja 2

Divulgaciones por año

  1. 2019: 1
  2. 2020: 0
  3. 2021: 2
  4. 2022: 1
  5. 2023: 0
  6. 2024: 1
  7. 2025: 0
  8. 2026: 47
Gravedad:
  1. NLTK: Quadratic CPU Exhaustion in `XMLCorpusView._read_xml_fragment()` esta entrada
    Media · 3.7 CVE-2026-81723 < 3.10.3 3.10.3
  2. Alta · 7.0 CVE-2026-81726 <= 3.10.3 sin corrección
  3. Alta CVE-2026-79674 < 3.10.3 3.10.3
  4. Alta · 8.8 CVE-2026-71513 >= 3.10.0, < 3.10.3 3.10.3
  5. Alta · 5.9 CVE-2026-62385 < 3.10.0 3.10.0
  6. Alta · 7.5 CVE-2026-33231 < 3.9.4 3.9.4
  7. Crítica · 10.0 CVE-2025-14009 < 3.9.3 3.9.3
  8. Alta · 7.5 CVE-2024-39705 < 3.9 3.9
  9. Alta · 7.5 CVE-2021-3842 < 3.6.6 3.6.6
  10. Alta · 7.5 CVE-2021-3828 < 3.6.4 3.6.4
  11. Alta · 7.5 CVE-2019-14751 < 3.4.5 3.4.5

Identifica las vulnerabilidades de tu aplicación

Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.

Conectar tu aplicación
Newsletter DockRay

La seguridad de tus aplicaciones en tu correo

Vulnerabilidades importantes, recomendaciones concretas y consejos prácticos para tus aplicaciones. Elige tus tecnologías y sigue lo que importa para tu proyecto.

  • Un resumen diario de nuevas vulnerabilidades, también las críticas.
  • Avisos sobre actualizaciones importantes.
  • Consejos prácticos sobre seguridad y monitorización de aplicaciones.

Déjalo vacío para recibir avisos de todas las tecnologías.

Suscripción gratuita. Date de baja con un clic en cualquier correo.