
Aviso: CVE-2026-38361 múltiplas vulnerabilidades de DoS (CWE-400/CWE-670) em dash-uploader (Python/PyPI)
Múltiplos problemas de Negação de Serviço (DoS) não autenticados em fohrloop/dash-uploader (Python, PyPI), incluindo (mas não se limitando a) queda do processo por falta de memória (OOM), truncamento de arquivos para zero bytes, esgotamento permanente de disco e contorno completo do limite documentado de max_file_size. Caminhos adicionais de abuso de recursos existem através do mesmo conjunto de parâmetros não sanitizados.
O repositório foi arquivado em 2025-07-19 sem mantenedor ativo. Todas as versões publicadas (0.1.0 até 0.7.0a2) são afetadas e continuarão sendo. O pacote ainda registra cerca de 28.000 downloads mensais.
Qualquer pessoa que execute o dash-uploader em produção deve aplicar uma mitigação por conta própria. A correção recomendada é migrar para o componente dcc.Upload embutido no Plotly Dash. Consulte Mitigação para conhecer todas as opções.
| CVE ID | CVE-2026-38361 (NVD) |
| Vulnerabilidade | Consumo Descontrolado de Recursos (CWE-400), Implementação de Fluxo de Controle Sempre Incorreta (CWE-670) |
| CVSS 3.1 | 7.5 / Alta (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| Produto | dash-uploader |
| Versões afetadas | 0.1.0 até 0.7.0a2 (todas as 18 versões) |
| Versão corrigida | nenhuma (projeto arquivado em 2025-07-19) |
| Vetor de ataque | Remoto, não autenticado |
| Descobridor | Muhammad Fitri Bin Mohd Sultan |
| Atribuído por | MITRE, 2026-05-07 |
| Relacionado | CVE-2026-38360 (travessia de caminho na mesma biblioteca) |
O manipulador HTTP do dash-uploader aceita requisições POST não autenticadas com parâmetros controlados pelo atacante que fluem para alocação de memória, operações de arquivo e criação de diretórios, sem verificações de limites, sem limites de taxa e sem mecanismo de limpeza. Quatro problemas independentes no mesmo caminho de código:
Verificado em um sistema com 7,7 GB: 5 requisições POST simultâneas com resumableTotalChunks=30000000 acionaram o OOM killer do Linux em 2 segundos. O log do kernel confirma:
Out of memory: Killed process 24203 (python3) total-vm:8302276kB, anon-rss:7068012kB
Cada requisição aloca aproximadamente 2,9 GB por meio de uma list comprehension sobre range(1, resumableTotalChunks + 1). O processo do servidor é encerrado e a aplicação fica completamente indisponível até reinicialização manual.
Um arquivo contendo 42 bytes de dados foi reduzido a 0 bytes por meio de uma única requisição POST com resumableTotalChunks=0. A causa raiz é o all() do Python retornar True para iteráveis vazios, o que engana o manipulador de upload fazendo-o tratar zero partes como um upload concluído. O arquivo existente é excluído via os.unlink() e substituído por um arquivo vazio.
10 diretórios temporários órfãos com arquivos de partes foram criados e persistidos indefinidamente no disco. Uma busca em todo o código-fonte por cleanup, ttl, expire, garbage, purge, cron, schedule e periodic retornou zero resultados. A única chamada de limpeza (shutil.rmtree) é executada exclusivamente em uploads concluídos. Não há mecanismo para recuperar espaço em disco de sessões incompletas.
max_file_size (verificado)O servidor aceitou uma parte de 5 MB para um arquivo declarando resumableTotalSize=999999999999 (~999 GB) com HTTP 200. O parâmetro max_file_size é passado apenas para o componente React JavaScript. O servidor nunca verifica o tamanho do arquivo, o tamanho da parte, o Content-Length ou o MAX_CONTENT_LENGTH do Flask. Um desenvolvedor que defina max_file_size=10 não tem proteção no lado do 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
...
O mesmo caminho de código produz tanto o OOM (resumableTotalChunks grande) quanto a primitiva de truncamento de arquivo (resumableTotalChunks=0).
Um atacante envia requisições POST não autenticadas para o endpoint /API/resumable.
resumableTotalChunks=30000000 alocam ~2,9 GB cada, acionando o OOM killer.resumableTotalChunks=0; o all([])=True do Python engana o servidor fazendo-o sobrescrever o arquivo alvo com conteúdo vazio.Nenhuma autenticação ou privilégio é necessário.
resumableTotalChunks)os.makedirs() com resumableIdentifier não sanitizadoall() do Python retornando True para iteráveis vazios quando resumableTotalChunks=0max_file_size é aplicado apenas no JavaScript do lado do cliente, enquanto o manipulador do lado do servidor não realiza nenhuma validação de tamanho e nunca define o MAX_CONTENT_LENGTH do Flaskdash_uploader/httprequesthandler.py (método BaseHttpRequestHandler._post)dash_uploader/upload.py (função Upload, parâmetro max_file_size)dash_uploader/configure_upload.py (sem MAX_CONTENT_LENGTH)Opções para usuários com implantações atuais, em ordem de preferência:
dcc.Upload, o componente oficial de upload distribuído com o Plotly Dash. Ele não possui parâmetro de contagem de partes, nenhum estado temporário em disco e respeita o MAX_CONTENT_LENGTH do Flask. Nenhum dos quatro problemas aqui se aplica. Mais adequado para arquivos pequenos e médios. Para uploads muito grandes, consulte o item 2.MAX_CONTENT_LENGTH), limites para qualquer contagem de partes fornecida pelo cliente e uma lista de permissões para nomes de arquivo aceitos.MAX_CONTENT_LENGTH do Flask no nível da aplicação (a biblioteca não o faz) e rejeite entradas no nível da aplicação ou do proxy reverso quando qualquer uma das seguintes condições for verdadeira:
resumableTotalChunks <= 0resumableTotalChunks exceder um limite razoável (por exemplo, 10.000)resumableTotalSize exceder o max_file_size configurado pelo desenvolvedor| Data | Evento |
|---|---|
| 2026-03-19 | Vulnerabilidades descobertas durante pesquisa de segurança em uma implantação de produção. |
| 2026-03-22 | Solicitação de CVE submetida à MITRE. |
| 2026-05-07 | CVE-2026-38361 atribuído pela MITRE. |
| 2026-05-07 | Aviso público publicado. |
| 2026-05-09 | Registro do CVE publicado no banco de dados de CVEs da MITRE e na NVD. |
0.6.1 (linha estável). Pré-lançamentos vão até 0.7.0a2.dash. Dependência opcional: pyyaml. Licença: MIT.Muhammad Fitri Bin Mohd Sultan