Foi divulgada uma vulnerabilidade que derruba um servidor Django por 1 minuto com apenas 20 MB de pacotes HTTP (CVE-2026-33033)
(new-blog.ch4n3.kr)Resumo geral em uma linha
Uma vulnerabilidade de exaustão de CPU pré-autenticação ocorre no MultiPartParser do Django quando o corpo de uma parte com Content-Transfer-Encoding: base64 é composto majoritariamente por espaços em branco, fazendo com que uma única requisição de cerca de 2,5 MB provoque um tempo de processamento mais de 2.100 vezes maior que o normal (CVE-2026-33033)
Resumo
- Pode ser acionada sem autenticação, mesmo em servidores com configuração padrão
- Como o middleware de CSRF acessa
request.POSTantes de a view ser executada, oMultiPartParseré disparado automaticamente; por isso, até endpoints autenticados já gastam vários segundos na etapa de validação de CSRF
- Como o middleware de CSRF acessa
- Uma única requisição de 20 MB pode ocupar um worker por cerca de 1 minuto
- Em uma configuração comum do gunicorn com 4 a 16 workers, apenas algumas dezenas de requisições simultâneas já podem praticamente paralisar o servidor
- O Django processa requisições
multipart/form-datacom oMultiPartParser, e como o middleware de CSRF acessarequest.POSTantes de entrar na view, esse parser sempre é executado mesmo sem autenticação - O núcleo da vulnerabilidade está em uma estrutura em que três camadas se multiplicam
- (Layer 1) while-loop de alinhamento do base64: ao remover os espaços em branco do chunk, o estado
remaining != 0é mantido, entãofield_stream.read(1)passa a ser chamado repetidamente para todo o restante do stream - (Layer 2) custo oculto O(C) de
LazyStream.read(1): a cada chamada deread(1), internamente um buffer de ~64 KB é retirado por inteiro e depois 65.535 bytes são empurrados de volta comunget(); esse padrão se repete continuamente - (Layer 3) concatenação de bytes O(C) em
unget(): um novo objeto é criado toda vez combytes + self._leftover
- (Layer 1) while-loop de alinhamento do base64: ao remover os espaços em branco do chunk, o estado
- Uma única requisição de 2,5 MB provoca internamente cerca de 86 GB de cópias de memória, ocupando completamente um worker por cerca de 5,3 segundos em um M2. Com 20 MB, leva cerca de 1 minuto
- Já existia um código de sanity check dentro de
unget()(_update_unget_history), mas neste ataque o tamanho dounget()segue um padrão monotonicamente decrescente, diminuindo 1 a cada chamada, então a condição de detecção (number_equal > 40) nunca é satisfeita - O ponto central do patch da equipe do Django foi mudar
read(4 - remaining)pararead(self._chunk_size), passando a ler 64 KB de cada vez em vez de 1 a 3 bytes. Com isso, o número de chamadas de leitura cai de 2,5 milhões para cerca de 40 - O valor padrão de
client_max_body_sizeno Nginx é 1 MB, mas ele costuma ser relaxado em endpoints de upload de arquivos; já o valor padrão deLimitRequestBodyno Apache httpd é 1 GB, então a defesa apenas com proxy não é garantida - A vulnerabilidade foi descoberta com o uso de Claude Code + Codex, e chama atenção o fato de um framework refinado ao longo de quase 20 anos ainda ter mantido um DoS pré-autenticação
4 comentários
Vamosss
Alguém aqui já testou isso na prática?
Um PoC criado para demonstração foi publicado no GitHub.
https://github.com/ch4n3-yoon/CVE-2026-33033-PoC
Muito bom.