Saltar al contenido
CVE-2026-121691 ·
Media · 10 de noviembre de 2026

Descompresión sin límites en el cargador de ZIP de whatstk agota la memoria y el disco

S0
Secur0 CNA
CVE-2026-121691

Descripción

Tratamiento incorrecto de datos muy comprimidos (CWE-409) en el cargador de ZIP de whatstk, en _str_from_file() (whatstk/whatsapp/parser.py), en whatstk desde 0.7.0 y anterior a 0.9.0. Quien aporta un export de chat puede agotar la memoria y el disco del proceso que lo analiza con un archivo de unas pocas decenas de kilobytes.

El cargador llama a zip_ref.extractall() sin ningún límite al tamaño de los miembros y sin comprobar la tasa de compresión, y después lee el fichero extraído por completo con f.read(). La única comprobación sobre el archivo cuenta los ficheros extraídos y falla cuando no hay exactamente uno, pero se ejecuta después de que extractall() ya los haya escrito en disco, de modo que no evita el coste que pretendía acotar.

El texto descomprimido se multiplica a continuación. _clean_text() lo pasa por unicodedata.normalize("NFKD", text), que mantiene una segunda copia, y el análisis construye sobre el resultado una lista de coincidencias de expresión regular y un DataFrame. El pico de memoria es por tanto un múltiplo del texto descomprimido y no del archivo, y un chat bien formado resulta peor que un bloque de relleno porque ejecuta el pipeline completo en lugar de fallar pronto en la detección de cabecera.

Un ZIP es una entrada documentada de WhatsAppChat.from_source() y df_from_whatsapp(), y es la forma en que WhatsApp exporta un chat en iOS, por lo que el atacante aporta el fichero completo y nada tiene que estar mal formado.

Tipo de vulnerabilidad (CWE)

  • CWE-409: Tratamiento incorrecto de datos muy comprimidos (amplificación de datos)

Versiones afectadas

whatstk desde 0.7.0 y anterior a 0.9.0. El soporte de ZIP llega en 0.7.0 y las versiones anteriores no leen archivos en absoluto. Estado por defecto: no afectado.

Medido en la 0.8.1 de PyPI con un archivo de 69,7 KB que contiene 20 MB de chat bien formado: pico de memoria residente de 405 MB y 5,9 segundos, frente a una línea base de 183 MB por importar la librería sola, de modo que unos 220 MB son atribuibles al análisis y la amplificación total respecto al archivo es de unos tres órdenes de magnitud. El mismo archivo en 0.9.0 sigue costando lo mismo, porque 20 MB queda por debajo del nuevo límite por defecto; un archivo que contiene 60 MB se rechaza en menos de 0,01 segundos sin materializar nada.

Puntuación (CVSS 4.0)

Media (6.7)

CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

El vector de ataque es Local porque la librería lee un fichero de export en la máquina que la ejecuta, que es su uso documentado; un servicio que acepte exports subidos y los analice es el caso en el que el vector sería de Red. Los privilegios requeridos son Ninguno, ya que quien aporta el archivo no necesita ningún acceso a esa máquina. Los requisitos de ataque son Ninguno porque el atacante escribe el archivo completo y no hace falta que coincida nada. La interacción del usuario es Activa porque el análisis tiene que invocarse sobre el fichero. La confidencialidad y la integridad son Ninguna: no se lee ni se altera nada. La disponibilidad del sistema vulnerable es Alta porque la memoria y el disco consumidos están acotados únicamente por el tamaño al que el atacante decida descomprimir, y el proceso muere cuando eso excede lo disponible. Las métricas del sistema subsiguiente son Ninguna, ya que lo que se detiene es el programa que realiza el análisis.

Solución

Actualizar a whatstk 0.9.0 o posterior, que rechaza por defecto una fuente mayor de 50 MB.

Conviene pasar un max_size más bajo cuando el análisis se realiza por cuenta de terceros, porque el límite por defecto sigue admitiendo una fuente única de 50 MB y el análisis cuesta un múltiplo de eso.

Parche

Publicado en 0.9.0, mediante la pull request #182 y el commit de publicación 01d66d8. El número de miembros se comprueba antes de leer nada, de modo que un archivo mal formado no cuesta nada. El miembro se lee después directamente del archivo mediante zip_ref.open() y un lector acotado, por lo que no se escribe nada en disco y un archivo no puede llenar el sistema de ficheros. Un nuevo argumento max_size, de 50 MB por defecto, se aplica también a ficheros locales, URLs y fuentes de Google Drive además de a los miembros del ZIP. El ZipInfo.file_size declarado se emplea sólo como primer rechazo económico y la corrección no depende de él, que es lo acertado: ese campo proviene del archivo y lo controla el atacante.

El límite acota lo que puede entrar, no lo que cuesta analizarlo por byte. El coste residual queda registrado por el mantenedor en la issue #186.

Créditos

  • Jorge Rey Cotallo - descubridor
  • Xoán M. Otero Jorge - analista
  • Darío Rivas - analista
  • Secur0 CNA - coordinador
  • Lucas Rodés-Guirao - desarrollador de la corrección

Origen del descubrimiento: Externo

Registro oficial: CVE-2026-121691

Registros relacionados

  • CVE-2026-121577 - separadores de cabecera sin escapar que construyen una expresión regular con retroceso exponencial en la misma librería