GHSA-6hm5-jgcp-p838 Natural Language Toolkit (NLTK): Path Traversal in NKJPCorpusReader leads to Arbitrary File Read and bypasses the nltk.pathsec sandbox (ENFORCE=True)
SummaryA 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:The header view appends a fixed basename and passes the string straight into
the corpus view (which opens it with the builtin `open()`):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()`: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.PoCTested against `nltk==3.9.4` (latest PyPI release) and current `develop`.`poc.py`:Output (unmodified):With `ENFORCE = True`, NLTK opened a file outside the corpus root via the
   builtin `open()` (no `PermissionError`, no warning) and returned its content.ImpactThis is a path traversal (CWE-22) leading to arbitrary file read. Any
   application that passes attacker-influenced values into `NKJPCorpusReader`'s
   `fileids` (e.g. letting a user choose which corpus document to read) is
   affected; the attacker can escape the corpus root and read files elsewhere on
   the host, defeating the `ENFORCE=True` sandbox.Honest scoping: `header()` discloses the content of out-of-root files named
   `header.xml` containing NKJP header XML. `raw()`/`words()`/`sents()` also open
   and read an arbitrary out-of-root file (proven by intercepting `open()`), but a
   separate pre-existing bug in `XML_Tool` (writing `str` to a binary
   `NamedTemporaryFile`) suppresses their return value on current Python, so for
   those modes the impact is arbitrary file open/read. The attacker chooses the
   directory freely; a fixed basename is appended per mode. The same
   "build-path-then-builtin-open, skipping pathsec" anti-pattern also appears in
   `xmldocs.py:161`, `util.py:212,215`, `crubadan.py:78,97`, `lin.py:43`,
   `ipipan.py:191`, `pl196x.py:110` and is worth fixing as a class.
CVSS v3.1 score: 7.5