CVE-2026-65915: NLTK: dead sandbox check enables arbitrary file read

GHSA-72r2-7mfr-5xr9 MEDIUM CISA: TRACK*
Published September 8, 2026
CISO Take

A patched-but-broken guard in NLTK's FileSystemPathPointer.open() means the 'security fix' compares a normalized path against itself, which is always true, so the block never fires — any string reaching nltk.data.load() or nltk.data.find(), including a file:// URL, can pull back the contents of any file the process can read. With 2,997 downstream dependents and NLTK embedded in NLP preprocessing pipelines, notebook servers, and multi-tenant AI services, the blast radius for apps that expose corpus or resource selection to user input is significant. The EPSS score is low in absolute terms (0.36%) but still sits in the top 71st percentile of scored CVEs, there is no CISA KEV listing and no public exploit or Nuclei template yet, so this is not under active mass exploitation — but the bug takes zero skill to trigger, just one file:// string, once an attacker finds an entry point. Patch to NLTK 3.10.0 now, and in the interim audit every code path where user-controlled strings reach nltk.data.load()/find() and hard-block the file:// scheme or allowlist known corpus names.

Sources: NVD GitHub Advisory EPSS OpenSSF ATLAS

What is the risk?

CVSS 6.5 (medium) reflects high confidentiality impact with no integrity or availability loss, network attack vector, low complexity, and low privileges required — but real-world risk hinges entirely on whether an application exposes the string passed to nltk.data.load()/find() to untrusted input. Where it does, exploitation is trivial (a crafted file:// URL, no auth bypass tricks, no chaining needed) and reliable across platforms. EPSS at 0.36% (71st percentile) and absence from CISA KEV indicate no evidence of active exploitation yet, and there's no public PoC beyond the disclosure writeup or scanner template, so this is a 'patch on the next cycle, prioritize if you know you're exposed' item rather than a fire drill. The package's broader risk profile (OpenSSF Scorecard 5.7/10, 48 other CVEs in the same package) suggests NLTK warrants ongoing scrutiny beyond this single issue.

How does the attack unfold?

Entry point
Attacker locates an application that passes a user-influenced string into nltk.data.load() or nltk.data.find(), such as a corpus/resource selection parameter.
AML.T0049
Exploitation
Attacker submits a file:// URL (e.g., file:///etc/passwd or a path to an app secrets file) as the resource identifier; the dead sandbox check in FileSystemPathPointer.open() never raises, so the file opens.
AML.T0037
Impact
The application returns or logs the file's raw contents, exposing credentials, private keys, or environment secrets to the attacker.
AML.T0055

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Jupyter Notebook pip <= 3.9.3 3.10.0
13.3K OpenSSF 5.7 3.0K dependents Pushed 9d ago 83% patched ~108d to patch Full package profile →

Do you use Jupyter Notebook? You're affected.

How severe is it?

CVSS 3.1
6.5 / 10
EPSS
0.4%
chance of exploitation in 30 days
Higher than 30% of all CVEs
Exploitation Status
Exploit Available
Exploitation: MEDIUM
Sophistication
Trivial
Exploitation Confidence
medium
CISA SSVC: Public PoC
Composite signal derived from CISA KEV, VulnCheck KEV, CISA SSVC, EPSS, Metasploit, Exploit-DB, trickest/cve, Nuclei templates, and inthewild.io exploitation reports.

What is the attack surface?

AV AC PR UI S C I A
AV Network
AC Low
PR Low
UI None
S Unchanged
C High
I None
A None

What should I do?

1 step
  1. Upgrade to NLTK >= 3.10.0, which fixes the sandbox check. Where upgrading isn't immediate: audit every call site of nltk.data.load() and nltk.data.find() for user-influenced arguments, and either eliminate that influence or strictly allowlist against a fixed set of known-good corpus/resource names — never pass raw user input, and explicitly reject any value containing 'file://', '/', or '..'. Run the process with least-privilege file permissions so a successful read has minimal value even if the check is bypassed. For detection, monitor for anomalous file reads (especially of /etc/passwd, .env, credential files, or SSH keys) originating from processes that import nltk, and alert on file:// scheme strings appearing in application logs or request parameters tied to NLTK resource loading.

What does CISA's SSVC say?

Decision Track*
Exploitation poc
Automatable No
Technical Impact partial

Source: CISA Vulnrichment (SSVC v2.0). Decision based on the CISA Coordinator decision tree.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity
NIST AI RMF
MEASURE 2.7 - AI system security and resilience is evaluated and documented
OWASP LLM Top 10
LLM05:2025 - Supply Chain Vulnerabilities

Frequently Asked Questions

What is CVE-2026-65915?

A patched-but-broken guard in NLTK's FileSystemPathPointer.open() means the 'security fix' compares a normalized path against itself, which is always true, so the block never fires — any string reaching nltk.data.load() or nltk.data.find(), including a file:// URL, can pull back the contents of any file the process can read. With 2,997 downstream dependents and NLTK embedded in NLP preprocessing pipelines, notebook servers, and multi-tenant AI services, the blast radius for apps that expose corpus or resource selection to user input is significant. The EPSS score is low in absolute terms (0.36%) but still sits in the top 71st percentile of scored CVEs, there is no CISA KEV listing and no public exploit or Nuclei template yet, so this is not under active mass exploitation — but the bug takes zero skill to trigger, just one file:// string, once an attacker finds an entry point. Patch to NLTK 3.10.0 now, and in the interim audit every code path where user-controlled strings reach nltk.data.load()/find() and hard-block the file:// scheme or allowlist known corpus names.

Is CVE-2026-65915 actively exploited?

No confirmed active exploitation of CVE-2026-65915 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-65915?

Upgrade to NLTK >= 3.10.0, which fixes the sandbox check. Where upgrading isn't immediate: audit every call site of nltk.data.load() and nltk.data.find() for user-influenced arguments, and either eliminate that influence or strictly allowlist against a fixed set of known-good corpus/resource names — never pass raw user input, and explicitly reject any value containing 'file://', '/', or '..'. Run the process with least-privilege file permissions so a successful read has minimal value even if the check is bypassed. For detection, monitor for anomalous file reads (especially of /etc/passwd, .env, credential files, or SSH keys) originating from processes that import nltk, and alert on file:// scheme strings appearing in application logs or request parameters tied to NLTK resource loading.

What systems are affected by CVE-2026-65915?

This vulnerability affects the following AI/ML architecture patterns: NLP preprocessing pipelines, notebook servers, multi-tenant AI pipelines, training pipelines.

What is the CVSS score for CVE-2026-65915?

CVE-2026-65915 has a CVSS v3.1 base score of 6.5 (MEDIUM). The EPSS exploitation probability is 0.36%.

What is the AI security impact?

Affected AI Architectures

NLP preprocessing pipelinesnotebook serversmulti-tenant AI pipelinestraining pipelines

MITRE ATLAS Techniques

AML.T0037 Data from Local System
AML.T0055 Unsecured Credentials

Compliance Controls Affected

EU AI Act: Article 15
NIST AI RMF: MEASURE 2.7
OWASP LLM Top 10: LLM05:2025

What are the technical details?

Original Advisory

### Summary There's a logic bug in `FileSystemPathPointer.open()` inside `nltk/data.py` that makes the sandbox check permanently inert. The guard condition is always `False` — meaning any file the process can read is accessible by passing a `file://` URL to `nltk.data.load()`. --- ### Details In `nltk/data.py`, `FileSystemPathPointer.open()` was patched at some point with a comment saying "SECURITY PATCH ENFORCING SANDBOX", but the check doesn't work: ```python def open(self, encoding=None): path = os.path.normpath(self._path) # Block raw absolute reads such as "/" "C:\\Windows" etc. if os.path.isabs(path) and path != os.path.normpath(self._path): raise ValueError(f"Direct absolute file access blocked: {path}") stream = open(self._path, "rb") ``` `path` is set to `os.path.normpath(self._path)` on line 1, then compared against `os.path.normpath(self._path)` again in the condition. They are always equal. The `ValueError` never fires. On top of that, `__init__` already calls `os.path.abspath()` before storing `self._path`, so it's normalized before `open()` is even called. Running `normpath` on it again changes nothing. The `stream = open(self._path, "rb")` line is always reached regardless of what path was passed in. --- ### PoC Tested on Python 3.11, NLTK 3.9.1, Ubuntu 22.04. ```python import nltk from nltk.data import FileSystemPathPointer # direct construction ptr = FileSystemPathPointer("/etc/passwd") with ptr.open() as f: print(f.read(300)) # via load() using file:// URL data = nltk.data.load("file:///etc/passwd", format="raw") print(data[:300]) ``` Both print file contents. No exception is raised. --- ### Impact Any app that lets users influence the string passed to `nltk.data.load()` or `nltk.data.find()` is exposed — web APIs, notebook servers, multi-tenant pipelines. An attacker can read any file the process user has access to: `/etc/passwd`, `.env` files, private keys, `~/.aws/credentials`, etc. ## Suggested Fix **File:** `nltk/data.py` — `FileSystemPathPointer.open()` (lines 378–390) ### What's wrong Line 387 compares `normpath(self._path)` against itself — always equal, so the `ValueError` never fires. The check is dead code. `__init__` already calls `abspath()` on construction, so re-running `normpath` inside `open()` changes nothing either. --- ### Fix Validate against the actual list of permitted data directories instead: ```python def open(self, encoding=None): import nltk.data as _d allowed = [os.path.abspath(p) for p in _d.path if p] if allowed and not any( os.path.commonpath([self._path, r]) == r for r in allowed ): raise ValueError( f"Access outside nltk_data blocked: {self._path!r}" ) stream = open(self._path, "rb") if encoding is not None: stream = SeekableUnicodeStreamReader(stream, encoding) return stream ``` --- ### Why `commonpath` not `startswith` `startswith` is bypassable by a path that shares a prefix: ``` /tmp/nltk_data_evil".startswith("/tmp/nltk_data") → True ✗ commonpath(["/tmp/nltk_data_evil", "/tmp/nltk_data"]) → "/tmp" ✓ ``` --- ### Diff ```diff - path = os.path.normpath(self._path) - if os.path.isabs(path) and path != os.path.normpath(self._path): - raise ValueError(f"Direct absolute file access blocked: {path}") - + import nltk.data as _d + allowed = [os.path.abspath(p) for p in _d.path if p] + if allowed and not any( + os.path.commonpath([self._path, r]) == r for r in allowed + ): + raise ValueError(f"Access outside nltk_data blocked: {self._path!r}") stream = open(self._path, "rb") ```

Exploitation Scenario

A SaaS text-analytics API lets authenticated users select which NLTK corpus or tokenizer resource to use for their document processing job via a request parameter. An attacker, holding only a low-privilege account, submits a job with the resource parameter set to `file:///app/.env` instead of a valid corpus name. The application calls nltk.data.load() with that string; the dead sandbox check never raises, the file opens, and its contents are returned in the job output or error response — handing the attacker the app's database credentials and API keys without ever touching an actual NLTK corpus.

Weaknesses (CWE)

CWE-284 — Improper Access Control: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.

  • [Architecture and Design, Operation] Very carefully manage the setting, management, and handling of privileges. Explicitly manage trust zones in the software.
  • [Architecture and Design] Compartmentalize the system to have "safe" areas where trust boundaries can be unambiguously drawn. Do not allow sensitive data to go outside of the trust boundary and always be careful when interfacing with a compartment outside of the safe area. Ensure that appropriate compartmentalization is built into the system design, and the compartmentalization allows for and reinforces privilege separation functionality. Architects and designers should rely on the principle of least privilege to decide the appropriate time to use privileges and the time to drop privileges.

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Timeline

Published
September 8, 2026
Last Modified
September 8, 2026
First Seen
September 8, 2026

Related Vulnerabilities