Link following in Natural Language Toolkit - #VU159488
Published: November 5, 2026
Vulnerability details
The vulnerability allows an attacker who controls a YCOE corpus loaded by a victim application to disclose file content outside that corpus root but inside an allowed NLTK data sandbox. Natural Language Toolkit's YCOECorpusReader can return this content through parsed_sents(), tagged_words(), tagged_sents(), words(), and related methods despite pathsec.ENFORCE=True. In shared-data or automated-ingestion deployments, exposed content can include other corpora and sensitive temporary files in the shared system temporary directory, which is a default allowed root. The demonstrated impact is read-only cross-corpus disclosure, with no integrity or availability impact.
The vulnerability exists due to lexical-only path containment checks combined with missing parent-root validation when constructing nested corpus readers. A remote attacker can supply an attacker-controlled YCOE corpus containing a directory symlink named psd or pos that points outside the corpus root but inside an allowed data root, provided the victim application loads that corpus. YCOECorpusReader constructs nested readers using self.root.join(), while FileSystemPathPointer.join() checks only a normalized lexical prefix and does not resolve symlinks; FileSystemPathPointer initialization likewise uses abspath without resolving links. CorpusReader.__init__ then validates the nested root without required_root, so only the global sandbox check runs and accepts the resolved in-sandbox target instead of enforcing containment within the enclosing YCOE corpus.
Subsequent read-time validation uses the nested reader's already-escaped root as required_root, and find_corpus_fileids() also anchors its resolved-root containment check to that target, so neither restores the original corpus boundary. The affected construction and validation logic resides in nltk/corpus/reader/ycoe.py, nltk/data.py, and nltk/corpus/reader/api.py. Ordinary non-symlink psd and pos directories remain contained, and symlinks resolving outside every allowed root are rejected during construction. This is therefore a per-corpus containment bypass within the global data sandbox, not an arbitrary-filesystem read.