CVE-2026-65915: NLTK before 3.10.0 Arbitrary File Read via FileSystemPathPointer
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 nltkdata 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/nltkdataevil".startswith("/tmp/nltkdata") → True ✗ commonpath(["/tmp/nltkdataevil", "/tmp/nltkdata"]) → "/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 nltkdata blocked: {self.path!r}") stream = open(self.path, "rb")
Other sources
NLTK versions before 3.10.0 contain a logic bug in FileSystemPathPointer.open() where the sandbox validation check compares a normalized path against itself, making the security check permanently inert. Attackers can pass file:// URLs to nltk.data.load() to read arbitrary files accessible to the process user, including credentials and configuration files.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/nltkto a version that resolves this vulnerability.Fixed in 3.10.0 - Upgrade
Upgrade
nltk/data.py — FileSystemPathPointer.open()to a version that resolves this vulnerability.Fixed in 3.10.0 - Configuration
In nltk/data.py (FileSystemPathPointer.open), replace the permanently-inert validation logic with an allowlist check against the real configured nltk data directories (compute allowed = [os.path.abspath(p) for p in _d.path if p], then require os.path.commonpath([self._path, r]) == r for r in allowed; if the check fails, raise ValueError like "Access outside nltk_data blocked: ...").
NLTK nltk.data.FileSystemPathPointer.open() Sandbox path validation = Validate against the actual permitted data directories using os.path.commonpath(...)
Event History
Frequently Asked Questions
Which deployments are exposed to this issue?
Deployments using NLTK versions before 3.10.0 are exposed when untrusted input can influence values passed to nltk.data.load(). The impact is limited to files readable by the account running the affected process.
What does an attacker need to exploit the flaw?
An attacker needs the ability to supply a file:// URL to nltk.data.load(). No user interaction is required, but the attacker must be able to influence that load target through the application.
Are confidentiality impacts limited to NLTK data files?
No. A successful exploit can read arbitrary files accessible to the process user, including credential and configuration files; the issue does not provide integrity or availability impact in the supplied assessment.
What should be done if upgrading is not immediately possible?
Do not allow untrusted input to reach nltk.data.load(), particularly file:// URLs. Run affected processes with least-privilege filesystem access so sensitive files are not readable by the process account.