CVE-2026-79676: NLTK before 3.10.3 Path Traversal via Symlink Bypass
Summary
Several corpus readers still step outside NLTK's symlink-aware trusted-root model. They derive in-root paths from trusted corpus state, convert those paths back into plain strings, and reopen them with built-in open() rather than nltk.pathsec.open().
Details
- Vulnerability type: Path traversal and symlink boundary bypass - Affected component: nltk.corpus.reader.ipipan, nltk.corpus.reader.crubadan, nltk.corpus.reader.lin - Affected versions: Published 3.9.4 and current source v3.10.0-rc2 both reproduced. - Patched versions: Not yet patched - Root cause: Root-derived paths are reopened with raw open() without preserving the trusted-root boundary.
IPIPANCorpusReader opens header.xml derived from morph.xml, CrubadanCorpusReader opens table.txt directly, and LinThesaurusCorpusReader opens simN.lsp paths returned from its own root helpers. Under pathsec.ENFORCE=True, a symlink placed inside the trusted corpus root can point outside the root and still be parsed successfully. It was confirmed parsed outside-root content is returned through public methods such as channels(), domains(), categories(), langs(), crubadantoiso(), synonyms(), and scoredsynonyms().
PoC
Preconditions - The application processes attacker-influenced corpora inside a trusted NLTK data root or trusted corpus directory.
Steps 1. Create a trusted corpus root and keep pathsec.ENFORCE=True with that root allowlisted. 2. Place symlinked reader inputs such as header.xml, table.txt, or simN.lsp inside the root and point them to external files. 3. Instantiate the corresponding corpus reader and call its normal public methods. 4. Observe that parsed outside-root values are returned even though pathsec.open() blocks the same symlink targets.
Minimal reproducible excerpt
text {'ipipan': ['LEAK', 'TOPSECRET', 'CLASSIFIED'], 'crubadan': ['LEAK'], 'lin': [('LEAK', 9.5)]}
Impact
An attacker who can stage corpus files or symlinks under a trusted data root can disclose outside-root content through normal corpus-reader results, defeating the boundary NLTK documents for shared and untrusted-input environments.
Remediation
Preserve PathPointer and requiredroot semantics end to end. Replace direct open() calls with nltk.pathsec.open() or a reader helper that keeps the trusted-root boundary intact.
References
- https://github.com/nltk/nltk/blob/3.9.4/nltk/corpus/reader/ipipan.py#L162-L192 - https://github.com/nltk/nltk/blob/3.9.4/nltk/corpus/reader/crubadan.py#L74-L98 - https://github.com/nltk/nltk/blob/3.9.4/nltk/corpus/reader/lin.py#L40-L43 - https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/pathsec.py#L521-L545
---
Fix + full-codebase audit (verified)
I swept every raw file open in the corpus readers, not just the three the umbrella named:
| Reader | Site | Advisory | Root scoping | |---|---|---|---| | crubadan | table.txt + <code>-3grams.txt | p4rw / j5pw | requiredroot=self.root | | lin | simN.lsp | p4rw | requiredroot=self.root | | xmldocs | XMLCorpusView bare-string fileid | 934p (base reader) | global fallback (view has no root) | | pl196x | textids index | found by audit | requiredroot=self.root | | mte | MTEFileReader | mvf5 | requiredroot threaded through 8 call sites | | toolbox | StandardFormat.open codecs.open | cr8c | global sandbox (low-level parser) | | namedentity | loadacefile ann/text | 7qj2 | global sandbox | | nkjp | XMLTool source file | p4rw class | requiredroot=self.root |
ipipan already validates via the earlier #3727 fix — unchanged.
Fix Each site now calls nltk.pathsec.validatepath(path, requiredroot=…) before opening. Where the reader has a concrete corpus root, the check is scoped with requiredroot (rejects any escape outside that root). XMLCorpusView carries no root, so it falls back to the global data-root sandbox via getattr(self, "root", None) — which also avoids an AttributeError on the bare-string path.
Reproduced (captured) raw open(symlink) reads: 'TOPSECRETOUTSIDEROOT' <- the bypass validatepath(symlink, requiredroot): ValueError -> BLOCKS the escape validatepath(legit in-root): PASSED <- loads normally
Honest residual The global-sandbox fallback (toolbox, namedentity, xmldocs-view) is only as tight as the allowed-roots list, which currently includes the system temp dir. Scoping every reader with requiredroot and removing the temp dir from the allowed roots would harden it further (separate advisory / task).
Tests testcorpusreaderpathsec.py — symlink escape rejected, in-root file allowed, XMLCorpusView string-fileid no AttributeError, MTEFileReader out-of-root rejected. 46 existing corpus/toolbox tests pass; all edited modules import (no circular import). pre-commit (black/isort/ruff) clean.
---
Scope caveat validatepath blocks every symlink escape variant (verified) and equals pathsec.open()'s guarantee, but does NOT block hardlinks (no symlink to resolve; tracked separately as GHSA-f794-5jv7-7672) or the validate-then-open TOCTOU race (shared by pathsec.open; needs ONOFOLLOW/openat).
Other sources
NLTK versions before 3.10.3 contain a path traversal vulnerability in corpus readers that reopen root-derived paths using built-in open() instead of nltk.pathsec.open(), allowing symlinks to escape trusted roots. Attackers who stage symlinked corpus files under a trusted data root can disclose outside-root content through normal corpus reader methods like channels(), domains(), and synonyms().
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/nltkto a version that resolves this vulnerability.Fixed in 3.10.3 - Upgrade
Upgrade
nltkto a version that resolves this vulnerability.Fixed in 3.10.3Patch NLTK before 3.10.3 Path Traversal via Symlink Bypass - Configuration
Ensure the trusted corpus/data-root enforcement is enabled by keeping `pathsec.ENFORCE=True` and allowlisting only the trusted root(s) (avoid including the system temp dir).
NLTK path security (pathsec.ENFORCE) ENFORCE = True - Configuration
Create a trusted corpus root and ensure each site calls `nltk.pathsec.validate_path(path, required_root=...)` before opening any corpus-derived path.
Application integration (NLTK pathsec boundary) required_root allowlisting = non-empty trusted root allowlist - Configuration
Where corpus readers reopen root-derived paths, replace direct built-in `open()` with `nltk.pathsec.open()` (or a reader helper that preserves the trusted-root boundary), and scope each reader with `required_root` (e.g., `required_root=self.root`) when the reader has a concrete corpus root.
NLTK corpus readers (crubadan/ipipan/lin and related) open() usage = replace built-in open with nltk.pathsec.open - Configuration
For view types like `XMLCorpusView` that carry no `_root`, ensure parsing does not fall back to the global data-root sandbox; instead, scope path validation/opening to an explicit `required_root` (the material notes XMLCorpusView has no root and falls back via `getattr(self, "_root", None)`).
NLTK XMLCorpusView root handling for file identifiers = use required_root/scoped root for views - Compensating control
Hardening: remove the temporary directory from the allowed roots list (the global sandbox fallback is only as tight as the allowed-roots list, which currently includes the system temp dir).
Event History
Frequently Asked Questions
Which deployments are exposed?
Deployments using NLTK versions before 3.10.3 are exposed when an attacker can place or stage symlinked corpus files beneath a trusted NLTK data root. The vulnerable behavior occurs in corpus readers that reopen root-derived paths.
What does an attacker need to exploit this issue?
The attacker needs the ability to stage symlinked corpus files under a trusted data root. No authentication or user interaction is required once those files are available to the affected corpus reader.
What information could be exposed?
An attacker can use symlinks that point outside the trusted root to disclose outside-root file contents through normal corpus reader methods, including channels(), domains(), and synonyms().
How can I determine whether my environment is affected?
Check whether NLTK is earlier than 3.10.3 and whether untrusted parties can create or influence corpus files or symlinks under a trusted NLTK data root. Review uses of affected corpus reader methods that may process those files.