CVE
CVE-2026-12259, CVE-2026-12261
CWE
CWE-284, CWE-494
Sistemas afectados
- Versiones del paquete PyPI `nltk` `<= 3.9.4`
- Aplicaciones, notebooks y pipelines que llaman a `nltk.download()` contra mirrors, proxies o servidores de índice personalizados menos confiables
- Directorios compartidos `~/nltk_data`, directorios de datos NLTK del sistema y cachés de compilación que almacenan corpus, tokenizadores o modelos de etiquetado para reutilizarlos entre proyectos
- Servicios de ML y NLP en Python que tratan los activos de corpus y modelos descargados como entradas confiables tras la instalación
Las divulgaciones de NLTK del 3 y 7 de agosto importan porque no son “solo” fallos de descargador de nicho. Se sitúan en el límite de confianza entre el paquete PyPI nltk y los artefactos de corpus o de modelo que muchas aplicaciones Python obtienen dinámicamente con nltk.download(). En nltk <= 3.9.4, ese límite podía cruzarse de dos formas distintas:
CVE-2026-12259:_download_package()podía escribir en disco los bytes del paquete descargado y, para cargas útiles ZIP, comenzar la extracción antes de aplicar las comprobaciones de integridad SHA-256 o MD5.CVE-2026-12261:_unzip_iter()extraía miembros ZIP a espacios de nombres compartidoscorpora/otaggers/sin exigir que los miembros del archivo pertenecieran realmente al paquete que se estaba instalando.
Juntos, estos dos fallos crean un pipeline clásico de “confianza después de la escritura”: NLTK podía hacer que el contenido hostil del paquete cobrara vida en disco primero y solo reconocer el problema de integridad después.
Paquete afectado y alcance práctico
El paquete de PyPI afectado es:
| Paquete | Rango afectado | Línea pública segura actual |
|---|---|---|
nltk | <= 3.9.4 | 3.10.x |
Esto no significa que toda aplicación que importe NLTK esté automáticamente comprometida. La ruta vulnerable se centra en el uso del descargador:
- código propio que llama a
nltk.download() - flujos de arranque de notebooks o CLI que descargan corpus automáticamente
- trabajos de CI que actualizan datos de NLTK durante la configuración de pruebas o entrenamiento
- entornos que usan mirrors personalizados, índices privados, proxies que interceptan tráfico o gateways de artefactos en caché
Si tu despliegue nunca descarga datos de NLTK en tiempo de ejecución y solo usa directorios de datos preinstalados, fijados y confiables, la exposición directa es mucho más limitada.
Por qué la ruta del descargador es crítica para la seguridad
Muchos equipos separan mentalmente “el paquete Python que instalamos desde PyPI” de “los archivos de corpus o modelo que ese paquete descarga después”. La ruta del descargador de NLTK muestra por qué esa distinción puede ser peligrosa.
Esos artefactos no son documentación inerte. Se convierten en entradas de:
- tokenizadores
- etiquetadores
- lectores de corpus
- pipelines de entrenamiento y evaluación
- notebooks y herramientas internas que asumen que el contenido de
nltk_dataes confiable
Así que, una vez que el descargador instala bytes de paquete envenenados, el resultado es efectivamente una cadena de suministro de paquetes secundaria dentro de la aplicación.
El flujo de datos vulnerable en la 3.9.4
El flujo de control clave en la 3.9.4 se ve así:
descargar los bytes del paquete desde info.url
-> escribir el archivo en download_dir/info.filename
-> si es ZIP, extraer en download_dir/info.subdir
-> hacer que los archivos de corpus/modelo cobren vida en disco
-> más tarde, _pkg_status() comprueba el checksum y el estado instalado
Ese “más tarde” es todo el problema.
CVE-2026-12259: la integridad ocurría después de la ruta de escritura
En la línea vulnerable, _download_package() dirigía el manejo de ZIP antes de la comprobación de estado posterior:
if info.filename.endswith(".zip"):
zipdir = os.path.join(download_dir, info.subdir)
if info.unzip or os.path.exists(os.path.join(zipdir, info.id)):
for msg in _unzip_iter(filepath, zipdir, verbose=False):
yield msg
Pero la lógica de checksum vivía en _pkg_status() después de que el archivo ya estuviera presente:
sha256_checksum = getattr(info, "sha256_checksum", None)
if sha256_checksum:
if sha256_hexdigest(filepath) != sha256_checksum:
return self.STALE
else:
md5_checksum = getattr(info, "checksum", None)
if md5_hexdigest(filepath) != md5_checksum:
return self.STALE
Eso significa que un proxy malicioso, un mirror comprometido u otra posición de sustitución de respuestas podía entregar bytes ZIP controlados por el atacante, dejarlos aterrizar en disco y potencialmente permitir que se descomprimieran en el directorio de datos antes de que NLTK decidiera que el paquete estaba obsoleto o no era válido.
En términos de aplicación, el control de integridad actuaba más como detección posterior a la instalación que como prevención previa a la instalación.
CVE-2026-12261: los espacios de nombres compartidos permitían que un paquete sobrescribiera a otro
El fallo de diseño más interesante es que la extracción ZIP no trataba el ID del paquete como el límite de propiedad. La extracción apuntaba al espacio de nombres compartido declarado por info.subdir:
zipdir = os.path.join(download_dir, info.subdir)
for msg in _unzip_iter(filepath, zipdir, verbose=False):
yield msg
Si el paquete declaraba subdir="taggers", entonces el archivo aterrizaba bajo .../taggers/. El código vulnerable no exigía que los miembros del archivo permanecieran dentro del directorio del paquete propietario bajo ese espacio de nombres.
Así que un paquete malicioso podía presentar entradas con la forma:
taggers/
averaged_perceptron_tagger_eng/
...archivos de modelo confiables...
incluso si el paquete que se estaba instalando no era averaged_perceptron_tagger_eng.
La cadena de explotación se ve así:
el atacante controla los bytes del paquete
-> el paquete declara un espacio de nombres compartido legítimo como taggers/
-> los nombres de los miembros del archivo apuntan al directorio de otro paquete dentro de ese espacio de nombres
-> el extractor escribe esos archivos en la ubicación en disco del paquete confiable
-> las APIs normales de NLTK cargan después el corpus o modelo envenenado
Por eso el aviso describe envenenamiento de recursos y modelos entre paquetes en lugar de path traversal. El atacante no necesita ../. El atacante solo necesita que el descargador confunda un espacio de nombres compartido con una raíz aislada por paquete.
Qué cambió la corrección de junio
La corrección upstream es técnicamente valiosa porque cierra ambas mitades de la cadena en lugar de parchear solo un síntoma.
Primero, NLTK empezó a verificar el archivo de descarga temporal antes de confirmarlo:
sha256_checksum = getattr(info, "sha256_checksum", None)
if sha256_checksum:
if sha256_hexdigest(tmp_filepath) != sha256_checksum:
_safe_remove(tmp_filepath)
yield ErrorMessage(info, f"Integrity check failed for {info.id!r}: sha256 mismatch")
return
validate_path(
filepath,
context="Downloader._download_package",
required_root=download_dir,
)
os.replace(tmp_filepath, filepath)
Eso cambia el límite de “escribir primero, quejarse después” a “fallar antes de publicar”.
Segundo, _unzip_iter() obtuvo un parámetro expected_root y el llamador ahora pasa el ID del paquete:
for msg in _unzip_iter(
filepath, zipdir, verbose=False, expected_root=info.id
):
yield msg
Dentro del extractor, NLTK ahora rechaza los miembros del archivo cuyo componente de ruta de nivel superior no coincide con el paquete propietario:
if expected_root is not None:
top = member.replace("\\", "/").lstrip("/").split("/")[0]
if top != expected_root:
yield ErrorMessage(
filename,
f"Cross-package overwrite blocked: member {member!r} "
f"is outside the owning package directory {expected_root!r}",
)
Esa es la forma correcta de la corrección. Preserva el diseño download_dir/subdir/ que el código de estado y de carga existente espera, pero restaura el verdadero límite de confianza a nivel de miembro de archivo.
Por qué debería importar a los equipos de AppSec
Este es un problema de cadena de suministro aunque la frase “gestor de paquetes” solo aparezca una vez en el flujo. El verdadero límite de confianza es:
wheel de PyPI para nltk
+ índice remoto de corpus/modelos
+ ruta de red del descargador
+ caché compartida en disco
+ consumidores posteriores de modelos/corpus
Eso importa en varios patrones empresariales comunes:
- infraestructura compartida de Jupyter o notebooks
- pipelines internos de ML que hidratan corpus a demanda
- trabajos de CI que descargan automáticamente corpus de prueba
- estaciones de trabajo de desarrolladores que reutilizan el mismo directorio
nltk_dataentre proyectos - proxies o mirrors de artefactos que terminan y reoriginan el tráfico de paquetes
Si un atacante puede envenenar la ruta de instalación de datos de NLTK, puede que no obtenga ejecución de código inmediata, pero aun así puede moldear flujos de clasificación, etiquetado, análisis, evaluación o sensibles a la reproducibilidad con contenido elegido por el atacante.
Detección y delimitación
Empieza identificando el uso de dependencias directas y los puntos de llamada del descargador:
python3 -m pip show nltk
rg -n 'nltk\\.download\\(' .
Luego busca hosts que almacenan activos de NLTK descargados en ubicaciones compartidas:
python3 - <<'PY'
import nltk
print(nltk.data.path)
PY
Las rutas de delimitación típicas incluyen:
~/nltk_data/usr/share/nltk_data- directorios de datos de NLTK locales del virtualenv
- cachés de proyecto copiadas en imágenes de CI
Para entornos donde el tráfico del descargador atraviesa un proxy o mirror personalizado, revisa:
- el
server_index_urlconfigurado - el comportamiento de interceptación TLS o del gateway de artefactos
- cualquier código de arranque personalizado que sustituya índices o fuentes de paquetes oficiales
Los casos históricos de mayor riesgo son las máquinas que:
- descargaron datos de NLTK durante el periodo vulnerable, y
- reutilizaron después esos datos para trabajos de producción, evaluación o desarrollo de modelos.
Guía de respuesta
- actualiza
nltka una versión corregida actual en la línea3.10.x; - elimina y repuebla los directorios en caché de
nltk_datadesde fuentes confiables; - revisa cualquier mirror, índice o configuración de proxy personalizado del descargador;
- trata los corpus y archivos de modelo descargados previamente como no confiables si se hidrataron a través de versiones afectadas en una ruta de red menos confiable.
Si un equipo quiere una ruta de recuperación conservadora, reconstruir la caché de datos de NLTK suele ser mejor que intentar razonar archivo por archivo sobre si alguna vez se introdujo un corpus o etiquetador envenenado.
Conclusión
CVE-2026-12259 y CVE-2026-12261 son un buen recordatorio de que los límites de seguridad no terminan en el artefacto de PyPI. En nltk <= 3.9.4, el descargador podía dar autoridad demasiado pronto al contenido remoto de paquetes, y luego colocar ese contenido en espacios de nombres compartidos de corpus y modelos en los que el código de aplicación posterior confiaba implícitamente.
Para los equipos de AppSec, la lección es sencilla: si una biblioteca descarga datos adyacentes a lo ejecutable, activos de modelo o contenido con forma de paquete después de la instalación, esa ruta de descarga merece el mismo modelado de amenazas que el gestor de paquetes original.
De la investigación a la remediación
Comprueba si este patrón existe en tu código
Convierte esta investigación en un flujo de remediación. Analiza dependencias y manifiestos para detectar riesgos similares en la cadena de suministro y prioriza las correcciones con contexto de alcanzabilidad.
Referencias
- Aviso de GitHub: GHSA-pv39-qrfq-g8gc / CVE-2026-12259
- Aviso de GitHub: GHSA-ffj6-66c4-86gw / CVE-2026-12261
- NVD: CVE-2026-12259
- NVD: CVE-2026-12261
- NLTK PR #3625: exige propiedad del paquete durante la extracción ZIP
- commit 7a5740a de NLTK: propiedad del descargador y comprobaciones de integridad previas a la extracción