Like leaving a booby-trapped shortcut in someone's filing cabinet — but you need the office key first
CVE-2026-62384 is a symlink-based sandbox escape in NLTK's FramenetCorpusReader, affecting versions 3.10.0 through 3.10.1. It bypasses the _reject_unsafe_path_component() fix shipped for CVE-2026-54292 by using symlinks whose names contain no path separators. When frame_by_name(), _lu_file(), or doc() resolve such a symlink via self.abspath() (a plain lexical join), they follow it outside the corpus root and return the contents of arbitrary XML-parseable files. The fix in 3.10.2 routes these paths through the symlink-resolving CorpusReader.open() with required_root. CWE-22 and CWE-59 both apply.
The vendor CVSS of 7.5 HIGH with vector AV:N/AC:L/PR:N/UI:N dramatically overstates the real threat. The chain *requires* filesystem write access to the corpus directory to plant the symlink — that is not network/unauthenticated access; it is local-privileged access. The sibling vuln CVE-2026-62383 in IPIPANCorpusReader was correctly scored at 5.5 MEDIUM with AV:L/PR:L. The same logic applies here. Additionally, FrameNet is a niche academic linguistics corpus — the fraction of NLTK's 65M monthly installs that actually instantiate FramenetCorpusReader is vanishingly small, and even when exploited the impact is confidentiality-only, limited to files that pass XML parsing. No integrity, no availability, no code execution.
4 steps from start to impact.
Attain filesystem write access to the corpus directory
FramenetCorpusReader treats as its corpus root. This typically means the attacker already has local shell access, has compromised an upload path that writes into the corpus directory, or has poisoned a corpus archive (tar/zip preserving symlinks) before download.- Local or application-level write access to the NLTK corpus data directory
- Operating system that supports symbolic links (Linux/macOS; limited on Windows without SeCreateSymbolicLinkPrivilege)
- Corpus directories are usually read-only in production; NLTK data is typically installed once via
nltk.download()and not user-writable - An attacker who already has filesystem write access to the host has far more impactful options than reading XML files through NLTK
- Windows default ACLs restrict symlink creation to administrators or Developer Mode
Plant a symlink with a clean name pointing outside the corpus root
frame/) whose filename contains no /, \, .., or drive-letter prefix. The symlink's target is an arbitrary path like /etc/passwd or /app/config/secrets.xml. The _reject_unsafe_path_component() guard passes the name because it only checks the *name string*, not the resolved destination.- Symlink creation privilege on the target filesystem
- Knowledge of the absolute path to a target file that is XML-parseable
- Target file must be valid XML or the NLTK XML parser will throw an
ElementTree.ParseErrorand return nothing useful - Many high-value secrets (private keys,
.envfiles, database credentials) are NOT valid XML — drastically limiting exfiltration value - Container/chroot environments may prevent symlink resolution outside the container filesystem namespace
Trigger a read via FramenetCorpusReader API
frame_by_name(), _lu_file(), or doc() with an argument matching the planted symlink name. The function resolves the path through self.abspath(), follows the symlink, and returns parsed XML content from outside the corpus root.- An application or interactive session that calls the vulnerable FramenetCorpusReader methods
- The method argument must match the symlink filename
- In most deployments, FramenetCorpusReader is called with hardcoded or internally-sourced frame names, not user-supplied input
- An attacker who can both write symlinks AND control API arguments already has extensive access to the host
- The FrameNet API is used almost exclusively in academic NLP research, not in internet-facing services
sys.addaudithook) can intercept open() calls to out-of-scope paths.Exfiltrate parsed XML content
- A channel to observe the return value of the corpus reader call
- If remote: the application must reflect parsed corpus data in its HTTP response
- Web applications almost never expose raw NLTK FrameNet query results to end users
- Even if reflected, the parsed XML structure may mangle or truncate the target file content
- Rate limiting and output encoding on web apps further reduce data exfiltration throughput
frame_by_name(), _lu_file(), and doc() through CorpusReader.open() with required_root, which resolves symlinks before validating the path. Pin nltk>=3.10.2 in your requirements.txt / pyproject.toml. No mitigation SLA applies at MEDIUM — go straight to the 365-day noisgate remediation SLA.nltk_data/corpora/framenet_v17/) to chmod 755 owned by root:root (or equivalent restrictive ACLs on Windows). This prevents unprivileged users from planting symlinks. Applies to all hosts running NLTK in your fleet.--no-same-owner and add a post-extraction step that removes symlinks: find /path/to/corpus -type l -delete. This breaks step 1 of the attack path for the supply-chain variant.--read-only flag and mount corpus data as read-only volumes. Container namespace isolation also prevents symlink resolution to host filesystem paths outside the container. This breaks both the symlink-planting and symlink-resolution steps.- WAF / network-layer controls — NLTK is a local Python library, not a network service. There is no HTTP request to inspect or block. WAF rules are irrelevant.
- Python virtual environment isolation — venvs isolate package installations but do NOT restrict filesystem reads. A symlink inside a venv's corpus dir still resolves to arbitrary paths on the host.
- Upgrading to NLTK 3.10.1 — This version is still vulnerable. The fix is in 3.10.2, not 3.10.1. The 3.10.1 release addressed unrelated issues.
The supporting signals.
| In-the-Wild Exploitation | None observed. Not listed on CISA KEV. No campaigns, threat actor TTPs, or incident reports reference this CVE as of 2026-10-08. |
|---|---|
| Proof-of-Concept Availability | No public PoC repositories found. Checked pocindex.io, GitHub (no repos named CVE-2026-62384), ExploitDB, and Nuclei templates. The GHSA advisory (GHSA-f833-7jw8-xwrv) describes the technique conceptually but no weaponized exploit code is circulating. |
| EPSS | 0.00651 (0.65%) — well below the 10th percentile threshold for likely exploitation. Consistent with a library-level bug that requires local access. |
| KEV Status | Not listed. No CISA KEV entry. No BOD 22-01 remediation deadline applies. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (vendor: 7.5 HIGH). noisgate assessment: the AV:N/PR:N claims are incorrect. Exploitation requires filesystem write access to plant symlinks, making the realistic vector AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N (5.5 MEDIUM), matching the score NVD assigned to sibling CVE-2026-62383. |
| Affected Versions | NLTK ≥ 3.10.0, < 3.10.2. Versions prior to 3.10.0 are affected by the predecessor CVE-2026-54292 instead (different path traversal, fixed in 3.10.0). Versions ≤ 3.9.x are not affected by *this specific* symlink bypass. |
| Fixed Version | NLTK 3.10.2 (commit 736d321, PR #3726). Also available in 3.10.3+. |
| Install Base / Exposure | NLTK has ~65 million monthly PyPI downloads. However, FramenetCorpusReader is an obscure academic component — the FrameNet corpus must be separately downloaded via nltk.download('framenet_v17'). No Shodan/GreyNoise/Censys exposure signatures exist because NLTK is a library, not a network service. |
| Disclosure Timeline | 2026-08-22 disclosed, 2026-09-08 GHSA published. Bypasses the fix for CVE-2026-54292 (patched 2026-05 in 3.10.0). Part of a cluster of NLTK corpus reader symlink issues (CVE-2026-62383, CVE-2026-62384, CVE-2026-62385, CVE-2026-79676). |
| Reporter | Not publicly attributed in the GHSA advisory. Likely the same researcher(s) who reported CVE-2026-54292 and the related corpus reader symlink cluster. |
Sources.
Why this verdict
- Misscored attack vector: The vendor vector claims
AV:N/PR:N(network, no privileges), but planting a symlink in the corpus directory requires *local filesystem write access* — the realistic vector isAV:L/PR:L, which alone drops the CVSS from 7.5 to 5.5. This matches the scoring of the identical sibling vuln CVE-2026-62383. - Compound prerequisite friction: The attacker must (1) have filesystem write to the corpus dir, (2) know a target file path that is valid XML, and (3) either directly invoke the API or control input to an app that calls it. An attacker with filesystem write has far more impactful options available — this vuln adds marginal value.
- XML-only exfiltration: The corpus reader parses the target file as XML via ElementTree. Non-XML files (private keys,
.env, database creds, password hashes) cause a parse error and leak nothing. This drastically narrows the confidentiality impact compared to an unrestricted file read. - Niche component within a popular package: NLTK's 65M monthly downloads are impressive, but
FramenetCorpusReaderrequires a separate 200MB corpus download and is used almost exclusively in linguistics research. The fraction of NLTK installs that instantiate this reader in production is negligible. - Role multiplier: NLTK is deployed on data-science workstations (low-value), ML pipeline application servers (typical), and occasionally in RAG/LLM preprocessing tiers (typical). It is *not* canonically a high-value-role component — it is not an identity provider, hypervisor, network edge appliance, backup system, CI/CD runner, or domain controller. In ML pipeline tiers, exploitation yields read access to XML files on that server — blast radius is host-level at worst, not domain/fleet/supply-chain scale. No high-value role floor applies.
- No exploitation signals: Zero in-the-wild activity, no KEV listing, EPSS 0.65%, no public PoC code. The attack is theoretical with no evidence of weaponization.
Why not higher?
The vulnerability is confidentiality-only with no code execution, no integrity impact, and no availability impact. It requires local filesystem write access — an attacker position that already implies significant compromise. The affected component (FramenetCorpusReader) is an obscure academic corpus reader within NLTK, not a widely-exercised API surface. NLTK is not deployed in any high-value infrastructure role where the blast radius would justify a HIGH floor.
Why not lower?
NLTK's enormous install base (65M/month) means even a niche component has a non-trivial absolute number of affected installations. The fix-bypass nature (circumventing CVE-2026-54292's patch) indicates a systemic code pattern issue that could recur. And in the specific scenario where a web application processes user-uploaded corpus archives that preserve symlinks, the attack chain shortens — though this remains a contrived deployment pattern. A LOW rating would understate the risk for the small population of NLP applications that do process external corpus data.
Crowdsourced verification payload.
Run on any host where NLTK is installed. Execute with python3 check_cve_2026_62384.py — no special privileges required. The script checks the installed NLTK version and whether the vulnerable FramenetCorpusReader code path exists.
#!/usr/bin/env python3
"""CVE-2026-62384 checker — NLTK FramenetCorpusReader symlink sandbox bypass.
Outputs VULNERABLE / PATCHED / UNKNOWN with exit codes 1 / 0 / 2.
"""
import sys
try:
import nltk
except ImportError:
print("UNKNOWN — nltk is not installed on this host.")
sys.exit(2)
version = getattr(nltk, '__version__', None)
if version is None:
print("UNKNOWN — could not determine nltk version.")
sys.exit(2)
try:
from packaging.version import Version
except ImportError:
# Fallback: simple tuple comparison
parts = tuple(int(x) for x in version.split('.')[:3])
if (3, 10, 0) <= parts < (3, 10, 2):
print(f"VULNERABLE — nltk {version} is in the affected range [3.10.0, 3.10.2).")
sys.exit(1)
elif parts >= (3, 10, 2):
print(f"PATCHED — nltk {version} contains the fix.")
sys.exit(0)
else:
print(f"PATCHED — nltk {version} predates the vulnerable code (3.10.0+). Note: check for CVE-2026-54292/CVE-2026-62385 on pre-3.10.0 versions.")
sys.exit(0)
else:
v = Version(version)
if Version('3.10.0') <= v < Version('3.10.2'):
print(f"VULNERABLE — nltk {version} is in the affected range [3.10.0, 3.10.2).")
sys.exit(1)
elif v >= Version('3.10.2'):
print(f"PATCHED — nltk {version} contains the fix.")
sys.exit(0)
else:
print(f"PATCHED — nltk {version} predates the vulnerable code (3.10.0+). Note: check for CVE-2026-54292/CVE-2026-62385 on pre-3.10.0 versions.")
sys.exit(0)