
Aviso: CVE-2026-38361 múltiples vulnerabilidades DoS (CWE-400/CWE-670) en dash-uploader (Python/PyPI)
Múltiples problemas de denegación de servicio (DoS) no autenticados en fohrloop/dash-uploader (Python, PyPI), incluidos, entre otros, la caída del proceso por agotamiento de memoria (OOM), la truncación de archivos a cero bytes, el agotamiento permanente del disco y la omisión completa del límite documentado de max_file_size. Existen vías adicionales de abuso de recursos a través del mismo conjunto de parámetros sin sanitizar.
El repositorio fue archivado el 2025-07-19 sin un mantenedor activo. Todas las versiones publicadas (de 0.1.0 a 0.7.0a2) están afectadas y lo seguirán estando. El paquete sigue obteniendo aproximadamente 28 000 descargas mensuales.
Cualquiera que ejecute dash-uploader en producción debe aplicar una mitigación por su cuenta. La solución recomendada es migrar al componente integrado dcc.Upload de Plotly Dash. Consulte Mitigación para conocer todas las opciones.
| ID de CVE | CVE-2026-38361 (NVD) |
| Vulnerabilidad | Consumo de recursos no controlado (CWE-400), Implementación de flujo de control siempre incorrecta (CWE-670) |
| CVSS 3.1 | 7.5 / Alta (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| Producto | dash-uploader |
| Versiones afectadas | de 0.1.0 a 0.7.0a2 (las 18 versiones) |
| Versión corregida | ninguna (proyecto archivado el 2025-07-19) |
| Vector de ataque | Remoto, no autenticado |
| Descubridor | Muhammad Fitri Bin Mohd Sultan |
| Asignado por | MITRE, 2026-05-07 |
| Relacionado | CVE-2026-38360 (recorrido de rutas en la misma biblioteca) |
El manejador HTTP de dash-uploader acepta solicitudes POST no autenticadas con parámetros controlados por el atacante que se utilizan en la asignación de memoria, operaciones de archivo y creación de directorios sin comprobaciones de límites, sin límites de tasa y sin mecanismo de limpieza. Cuatro problemas independientes en la misma ruta de código:
Verificado en un sistema de 7,7 GB: 5 solicitudes POST concurrentes con resumableTotalChunks=30000000 activaron el OOM killer de Linux en 2 segundos. El registro del kernel lo confirma:
Out of memory: Killed process 24203 (python3) total-vm:8302276kB, anon-rss:7068012kB
Cada solicitud asigna aproximadamente 2,9 GB mediante una comprensión de lista sobre range(1, resumableTotalChunks + 1). El proceso del servidor se termina y la aplicación queda completamente indisponible hasta un reinicio manual.
Un archivo que contenía 42 bytes de datos se redujo a 0 bytes mediante una única solicitud POST con resumableTotalChunks=0. La causa raíz es que all() de Python devuelve True para iterables vacíos, lo que engaña al manejador de subida para que trate cero fragmentos como una subida completada. El archivo existente se elimina mediante os.unlink() y se reemplaza por un archivo vacío.
Se crearon 10 directorios temporales huérfanos con archivos de fragmentos que persistieron indefinidamente en el disco. Una búsqueda en toda la base de código en todos los archivos fuente de cleanup, ttl, expire, garbage, purge, cron, schedule y periodic arrojó cero resultados. La única llamada de limpieza (shutil.rmtree) se ejecuta exclusivamente en subidas completadas. No existe ningún mecanismo para recuperar espacio en disco de sesiones incompletas.
max_file_size (verificada)El servidor aceptó un fragmento de 5 MB para un archivo que declaraba resumableTotalSize=999999999999 (~999 GB) con HTTP 200. El parámetro max_file_size se pasa únicamente al componente JavaScript de React. El servidor nunca comprueba el tamaño del archivo, el tamaño del fragmento, Content-Length ni MAX_CONTENT_LENGTH de Flask. Un desarrollador que establezca max_file_size=10 no tiene protección en el lado del servidor.
# dash_uploader/httprequesthandler.py
def _post(self):
resumableTotalChunks = request.form.get("resumableTotalChunks", type=int) # attacker-controlled, no bounds
...
chunk_paths = [
os.path.join(temp_dir, get_chunk_name(resumableFilename, x))
for x in range(1, resumableTotalChunks + 1) # unbounded; e.g. 30M -> ~2.9 GB -> OOM
]
upload_complete = all([os.path.exists(p) for p in chunk_paths]) # all([]) is True -> truncation when chunks=0
if upload_complete:
target_file_name = os.path.join(temp_root, resumableFilename)
if os.path.exists(target_file_name):
os.unlink(target_file_name) # existing file deleted
with open(target_file_name, "ab") as target_file:
for p in chunk_paths: # empty list -> empty file written
...
La misma ruta de código produce tanto la caída por OOM (resumableTotalChunks grande) como la primitiva de truncación de archivos (resumableTotalChunks=0).
Un atacante envía solicitudes POST no autenticadas al endpoint /API/resumable.
resumableTotalChunks=30000000 asignan ~2,9 GB cada una, activando el OOM killer.resumableTotalChunks=0; Python all([])=True engaña al servidor para que sobrescriba el archivo objetivo con contenido vacío.No se requiere autenticación ni privilegios.