Skip to content
NLTKGHSA-3hhw-38pf-pxj6

NLTK: Symlink-based arbitrary file read in IPIPANCorpusReader, bypasses nltk.pathsec entirely

Medium5.5CVE-2026-62383 · Published Sep 8, 2026

GitHub advisory

Affected versions

PackageAffectedFixed in
nltk
PyPI
>= 3.10.0, < 3.10.23.10.2
Details and references

## Summary `IPIPANCorpusReader` (`nltk/corpus/reader/ipipan.py`) exposes public methods, `channels()`, `domains()`, `categories()`, and `fileids(channels=...)`, that accept a caller supplied `fileids` list and read a file via a completely unprotected builtin `open()` call, with no `nltk.pathsec` involvement at all. A symlink placed inside the corpus root, with a name containing no separators or `..`, passes NLTK's existing traversal checks and is opened directly, reading a file from anywhere on the filesystem the process can access. ## Root cause All four methods route through `_get_tag()`: ```python def _get_tag(self, f, tag): tags = [] with open(f) as infile: # builtin open(), no pathsec involvement header = infile.read() ... ``` `f` arrives via `_list_header_files()` / `_list_morph_files_by()`, both of which call: ```python f.replace("morph.xml", "header.xml") ``` on the result of `self.abspath(...)` or `self.abspaths(...)`. `FileSystemPathPointer` subclasses `str`, so `.replace()` returns a plain Python string, silently discarding the `PathPointer` wrapper. That plain string is handed straight to builtin `open()`. This is a more severe variant of the same CWE-59 class already fixed elsewhere in this codebase (`CorpusReader.open()`, `NKJPCorpusReader.add_root()`, and the recent `FramenetCorpusReader` fix): those route file access through `nltk.pathsec.validate_path()`, at minimum the global, non-scoped check, before opening. Here, converting the `PathPointer` to a plain string before calling `open()` skips `pathsec` completely, not just the corpus-root-scoped check, so the symlink target does not even need to land under a registered `nltk.data.path` root. Plain literal `../` traversal in the fileid is still blocked by `FileSystemPathPointer.join()`, so this is specifically the symlink variant, not a regression of the older, simpler traversal class. ## Proof of concept Constructed the normal, documented way, `fileids` as a regex over file paths, so the reader auto-discovers whatever `.xml` files exist in its root with no special knowledge of the planted symlink. ```python import os import tempfile from nltk.corpus.reader.ipipan import IPIPANCorpusReader root = tempfile.mkdtemp() corpus_root = os.path.join(root, "ipipan") os.makedirs(corpus_root) with open(os.path.join(corpus_root, "real_morph.xml"), "w") as f: f.write("<channel>legit</channel>") secret_dir = os.path.join(root, "outside_ipipan_root") os.makedirs(secret_dir) secret_path = os.path.join(secret_dir, "stolen.xml") with open(secret_path, "w") as f: f.write("<channel>TOP-SECRET-CHANNEL-DATA-FROM-OUTSIDE-CORPUS-ROOT</channel>") os.symlink(secret_path, os.path.join(corpus_root, "evil_link.xml")) reader = IPIPANCorpusReader(corpus_root, r".*\.xml") print("Auto-discovered fileids:", sorted(reader.fileids())) result = reader.channels(fileids=["evil_link.xml"]) print(result) ``` Actual output when run against current `develop`: ``` Auto-discovered fileids: ['evil_link.xml', 'real_morph.xml'] ['TOP-SECRET-CHANNEL-DATA-FROM-OUTSIDE-CORPUS-ROOT'] ``` That content was read from `secret_path`, a file entirely outside `corpus_root`. No exception raised anywhere. The planted symlink even surfaces naturally in the reader's own `fileids()` listing, exactly as a real file would. Verified separately that literal `../` traversal in the fileid is still rejected (`ValueError: Traversal blocked`), confirming this is specifically the symlink gap, not a broader regression. ## Why this is in scope - No malicious file for a victim to open, no special user interaction. Just a tampered or shared corpus directory (`SECURITY.md` names "shared environments... multi-tenant pipelines" as the project's own stated threat model) plus a completely normal API call. - Core corpus-reader code, reached through plain `import nltk` and documented, programmatic usage (`words()`, `sents()`, `channels()`, etc.), not a demo or GUI tool. - Same reader catego

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

More NLTK advisories

All NLTK
DateAdvisory
Sep 8NLTK: Symlink escape in CorpusReader allows arbitrary local file read outside the corpus root
CVE-2026-70626High6.2fixed in 3.9.4
Sep 8NLTK: FileSystemPathPointer.open() sandbox check is dead code , arbitrary file read via file:// protocol
CVE-2026-65915Medium6.5fixed in 3.10.0
Sep 8NLTK: StreamBackedCorpusView Bypasses pathsec.ENFORCE - Arbitrary Local File Read
CVE-2026-63312Highfixed in 3.10.0
Sep 8NLTK: Missing Post-Download Integrity Verification Allows Malicious Package Injection
CVE-2026-12259Medium5.3fixed in 3.9.3
Sep 8NLTK: Stable FrameNet and NKJP readers parse outside-root XML
CVE-2026-62385High5.9fixed in 3.10.0
Sep 8NLTK: Symlink-based sandbox bypass in FramenetCorpusReader (bypasses the fix for CVE-2026-54292)
CVE-2026-62384High7.5fixed in 3.10.2

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.