CVE-2026-78681High▾ TwilightNLTK: Entity-expansion DoS (billion laughs) via remaining raw ElementTree parses
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 41.3 · likelihood 0.1 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Sep 2.
Disclosure to exploitation, from the record and what we observed since indexing it.
0.3%
Disclosed via OSV
high → none
none → high
high → none
none → high
high → none
none → high
high → none
none → high
high → none
none → high
high → none
none → high
Several XML parsing sites in NLTK still used xml.etree.ElementTree directly, which honours <!ENTITY> declarations in a document's internal DTD subset. A crafted document a few hundred bytes long can expand to megabytes in memory (each nesting level multiplies by ten), a denial-of-service.
Affected call sites (<= 3.10.2):
nltk.chunk.named_entity.load_ace_file — parses ACE annotation XMLnltk.internals.ElementWrapper — converts any given string to an Elementnltk.downloader — Package.fromxml, Collection.fromxml, _find_collections, _find_packageslibexpat 2.6.0 added an input-amplification cap, but it only engages above an activation threshold (~8 MiB output) and depends on whichever libexpat the interpreter links; builds against older libexpat have no cap at all. External entities are not resolved by ElementTree, so this is a memory-amplification DoS (CWE-776), not XXE/file disclosure.
This completes the earlier defusedxml adoption that these sites were missed by. Fix routes all of them through a new nltk.xmlsec module that refuses entity declarations, preferring defusedxml and falling back to a standard-library xml.parsers.expat pre-scan when defusedxml is absent.
Reproducible PoC against a real affected entry point (nltk.internals.ElementWrapper). Every number below is captured output, not illustrative.
xml.etree.ElementTree)A payload of a few hundred bytes expands to megabytes in memory. Each nesting level multiplies output by 10 while adding ~56 bytes of input:
| levels | input bytes | expanded bytes | factor |
|---|---|---|---|
| 3 | 218 | 10,000 | x45 |
| 4 | 274 | 100,000 | x364 |
| 5 | 330 | 1,000,000 | x3,030 |
| 6 | 386 | (libexpat 2.7.1 cap trips) | - |
The level-6 cap is libexpat's, not NLTK's: it only engages above an ~8 MiB activation threshold, and older libexpat builds (still shipped with many 3.10/3.11 interpreters) have no cap at all. Under the threshold — up to ~1 MB per parse here — expansion always succeeds.
import xml.etree.ElementTree as ET
def bomb(levels):
d = "\n".join(f'<!ENTITY e{i} "{("&e%d;"%(i-1))*10}">' for i in range(1, levels+1))
return f'<!DOCTYPE d [<!ENTITY e0 "AAAAAAAAAA">{d}]><d>&e{levels};</d>'
ET.fromstring(bomb(5)) # -> element whose .text is 1,000,000 chars
>>> from nltk.internals import ElementWrapper
>>> ElementWrapper(bomb(5))
EntitiesForbidden: EntitiesForbidden(name='e0', ...)
An entity declaration can hide behind a decoy <!DOCTYPE> in a prolog comment. Raw ElementTree still processes the real declaration and expands; a guard that walks the DOCTYPE text is fooled. The shipped guard re-parses with expat, so it is not:
evil = '<!-- <!DOCTYPE x [ ] > --><!DOCTYPE d [<!ENTITY a "PPPP...">]><d>&a;</d>'
raw ElementTree -> EXPANDS ('PPPPPPPPPPPP...', 40 chars)
nltk.xmlsec -> REJECTED (EntitiesForbidden)
An earlier draft of the fallback that walked the text was bypassed by this and 4 similar payloads (decoy DOCTYPE in a PI, stray ] inside a PI in the internal subset). All five are now regression tests.
nltk.xmlsec prefers defusedxml and falls back to a stdlib xml.parsers.expat pre-scan. Same payloads, defusedxml hidden to force the fallback:
stdlib fallback | billion-laughs -> REJECTED (EntitiesForbidden)
stdlib fallback | comment-decoy differential -> REJECTED (EntitiesForbidden)
Environment: python 3.13.7, libexpat 2.7.1. Confirmed identical amplification on python 3.10 (NLTK's floor).
nltk < 3.10.3Upgrade to a patched release:
nltk 3.10.3Field changes observed since this record was first indexed.
Connected by shared product, vendor, weakness, or advisory.
CVE-2026-80206HighNLTK: ReDoS in nltk.tgrep via unvalidated user-supplied regular expressions
CVE-2026-79676HighNLTK: Corpus readers follow symlinks outside trusted roots despite pathsec enforcement
CVE-2026-79657CriticalNLTK: Allowlisted pickle loaders still permit code execution in current source
CVE-2026-12876MediumNLTK: Uncontrolled resource consumption in RecursiveDescentParser via ambiguous or left-recursive grammars
CVE-2026-81723Low· 3.7NLTK: Quadratic CPU Exhaustion in `XMLCorpusView._read_xml_fragment()`
CVE-2026-78680High· 7.8NLTK: Uncontrolled search path when invoking the Graphviz 'dot' binary