GHSA-qg35-44qx-7vrr: NLTK: Symlink escape in YCOECorpusReader lets a psd/pos directory symlink read files outside the corpus root within the data sandbox

  • Severity: HIGH
  • CVE: CVE-2026-126304
  • Ecosystems: pip
  • Packages: nltk

## Summary

`YCOECorpusReader` builds two nested corpus sub-readers whose roots are derived from `self.root.join(“psd”)` and `self.root.join(“pos”)` (nltk/corpus/reader/ycoe.py). Because `FileSystemPathPointer.join()` (nltk/data.py) performs a purely lexical containment check and never resolves symlinks, and because `CorpusReader.__init__` (nltk/corpus/reader/api.py) validates a sub-reader root only against the global NLTK data sandbox and not against the enclosing corpus root, a directory symlink named `psd` or `pos` planted inside a YCOE corpus lets the reader anchor its nested sub-readers outside the YCOE corpus root. An attacker who controls the contents of a YCOE corpus loaded under `pathsec.ENFORCE=True` can make `parsed_sents()`, `tagged_words()`, and related methods return file content from other locations inside the allowed data sandbox, defeating the per-corpus-root containment boundary the pathsec controls are meant to enforce.

## Root Cause

NLTK’s pathsec controls are designed to keep each corpus reader confined to its own corpus root. The scoped containment is enforced through `validate_path(…, required_root=self._root)` at read time. However, the containment invariant is violated at reader-construction time for nested readers:

– `FileSystemPathPointer.join()` accepts a component that is a directory symlink because it only checks the joined string lexically (`os.path.normpath` plus a `startswith` prefix test) and never calls `resolve()`.
– `CorpusReader.__in…

Source: GitHub Security Advisories · 2026-11-13