PT-2026-147353
Publicado
2026-11-14
·
Atualizado
2026-11-14
Descrição por
CVSS 3.1
7.5
AltaMétricasAV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Summary
ConllCorpusReader.srl instances() builds semantic-role-labeling instances from a CoNLL corpus block. For a sentence block with P predicates and P span columns, the instance builder rescans every span column (and every span within it) once per predicate while searching for each predicate's head, giving O(P^2 * R) work. A single well-formed block that places the head span in the last column forces the full scan on every predicate, so an application that reads an attacker-supplied CoNLL SRL corpus can be driven into tens of seconds of single-threaded CPU. No authentication or user interaction is required.Root Cause
get srl instances iterates over the P predicates of a block; for each predicate it scans every span list and every span within it, using an inner break that stops only when a span tagged V or C-V containing the predicate word is found. When the sole head span is placed in the last span column, no earlier column matches, so the for ... else: continue construct traverses every span of every earlier column before matching. Over P predicates each scanning up to P columns of R spans, the cost is O(P^2 * R). The block is read whole with no size cap, and the grid-consistency check enforces only uniform column count, not a bound on P, R, or total work.text
Affected function:
- nltk.corpus.reader.conll.ConllCorpusReader. get srl instances (nltk/corpus/reader/conll.py:394-424; nested rescan at lines 405-419)
Public entry point: nltk.corpus.reader.conll.ConllCorpusReader.srl instances (nltk/corpus/reader/conll.py:158-169).Impact
An attacker who can supply a CoNLL corpus that an application loads and iterates with
srl instances() causes sustained single-threaded CPU consumption (availability loss). The path is network-reachable through any corpus-ingestion surface (AV:N) and needs no privileges or user interaction (PR:N, UI:N). Because the cost is super-linear in attacker-chosen predicate and column counts, a roughly 1 MB crafted corpus pins a core for tens of seconds and a few megabytes reaches minutes. Confidentiality and integrity are not affected. The NLTK regular-expression wall-clock timeout wraps regex matching only and does not cover this pure-Python nested loop over the parsed grid. Exploitation requires the application to use the SRL-specific API on untrusted CoNLL data with words, pos, tree, and srl columns; a deployment that never calls srl instances() is unaffected.Proof of Concept
text
Environment: nltk 3.10.3, CPython. Stage a crafted CoNLL corpus under an authorized data root and iterate srl instances().
Corpus construction: one blank-line-delimited block with P predicate rows and P SRL span columns, the sole "V" head span in the last column.python
import time, os, tempfile
from nltk.corpus.reader.conll import ConllCorpusReader
def build block(n):
# n predicates, n SRL columns; the single V head placed in the last column
# (columns: words pos tree srl...): emit n predicate rows and n SRL span
# columns, with the single "V" head span placed in the final column.
...
for n in (40, 80, 160, 320):
root = tempfile.mkdtemp()
open(os.path.join(root, "c.conll"), "w").write(build block(n))
r = ConllCorpusReader(root, ["c.conll"], ("words", "pos", "tree", "srl"))
t = time.perf counter()
list(r.srl instances())
print(n, round(time.perf counter() - t, 3), "s")text
Negative control (single predicate): n=40 -> 1.88 ms ; n=320 -> 4.45 ms (flat)
Attack (P=R=n): n=40 -> 0.022s (10 KB) | n=80 -> 0.148s (39 KB) | n=160 -> 1.164s (156 KB) | n=320 -> 10.002s (619 KB)The attack timing rises about 8x per doubling of n while the file grows only about 4x, confirming super-linear cost driven by attacker-chosen predicate and column count; the single-predicate control stays in the millisecond range.
Attack Chain
- Exposure: An application loads an attacker-supplied CoNLL corpus (columns include
srl) and iterates it with the publicConllCorpusReader(...).srl instances(). - Control: The attacker controls the predicate count P, the span-column count, and the placement of the single head span; the block is read whole with no size cap.
- Path:
srl instances()callsget srl instances, which pairs each predicate against the span columns via a doubly-nested scan. - Guard: The inner
breakon the first matchingVorC-Vhead is defeated by placing the sole head span in the last column, so every earlier column is scanned in full. The path-containment sandbox bounds where files load, not how much work a block costs. - Primitive: The nested rescan performs O(P^2 * R) span comparisons with no work cap or timeout.
- Result: Sustained single-core CPU exhaustion; a roughly 1 MB corpus costs tens of seconds and scales super-linearly, blocking the processing thread.
Bypass Evidence
The only short-circuit on this path is the inner
break that fires when a predicate's V or C-V head span is found. It is defeated by construction: placing the sole head span in the last span column means no earlier column matches, so the for ... else: continue traverses every span of every earlier column for every predicate. This was confirmed in source (nltk/corpus/reader/conll.py:405-419) and reproduced end-to-end through the real public srl instances() API, where the attack timing scales super-linearly while a single-predicate control stays flat. The path-containment sandbox is a file-location control and imposes no bound on parse cost, and the regular-expression timeout does not wrap this loop. The strongest disproof attempts (that Python range membership is O(1) so the loop is linear, and that the break fires early) both fail: the O(1) membership test is evaluated once per span, and the number of spans traversed is exactly what the last-column head placement maximizes.Affected Versions
Ecosystem: PyPI (pip)Package: nltkConfirmed affected range: <= 3.10.3Latest release checked: 3.10.3 (PyPI, https://pypi.org/pypi/nltk/json)Fix status: 3.10.4
The nested rescan ships in every release carrying this reader 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; the behavior predates the recent regular-expression hardening.
Suggested Fix
Replace the per-predicate linear search over all span columns with a single pass that indexes head spans by word position (for example, build a map from word index to the owning span column once, then look up each predicate in constant time), reducing the total cost to about O(P * R). As defense in depth, bound the predicate and span counts accepted from a single block and process untrusted corpora under a CPU or time budget. No configuration-only mitigation removes the exposure for a service that must read untrusted CoNLL SRL data; until a fix ships, callers should cap accepted corpus size and isolate the parsing worker.
Reported by zx (GitHub: @manus-pi).
Fix status (2026-09-28)
PR: #3753 "[Do not merge yet] Fix GHSA-8mgp-746c-j5xp: URL-scheme bypass of pathsec.validate path", open; head da55cab0e at the time of this note (a develop merge on top of 238f81500, the head on which the verdicts below were measured).
Fix commit: cf12a5e6a "Patch seven algorithmic-DoS and path/exec GHSA classes across all affected modules" (ConllCorpusReader. get srl instances builds a one-pass wordnum to spanlist index instead of rescanning per predicate).
Probe: nltk/test/unit/security probes/ghsa v8f3 6phw 6mh9.py (on the PR). Verdict on the PR head: FIXED (srl instances scaled 3.9x over 4x predicates). The same probe run against develop 2fa3f48fe: VULNERABLE (45.8x over 4x predicates).
Explorabilidade
Correção
Impactos
Fornecedores
Produtos afetados
Identificadores relacionados
Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾