Se divulga una vulnerabilidad que deja inactivo un servidor Django durante 1 minuto con apenas un paquete HTTP de 20 MB (CVE-2026-33033)
(new-blog.ch4n3.kr)Resumen general en una línea
Una vulnerabilidad de agotamiento de CPU preautenticación en MultiPartParser de Django ocurre cuando el cuerpo de una parte con Content-Transfer-Encoding: base64 contiene principalmente espacios en blanco, y una sola solicitud de unos 2.5 MB provoca un tiempo de procesamiento más de 2,100 veces mayor de lo normal (CVE-2026-33033)
Resumen
- Puede activarse sin autenticación, incluso en servidores con configuración predeterminada
- Como el middleware CSRF accede a
request.POSTantes de entrar a la view,MultiPartParserse ejecuta automáticamente, por lo que incluso en endpoints autenticados ya se consumen varios segundos durante la etapa de verificación CSRF
- Como el middleware CSRF accede a
- Una sola solicitud de 20 MB ocupa un worker individual durante alrededor de 1 minuto
- En una configuración típica de gunicorn con 4 a 16 workers, apenas unas decenas de solicitudes simultáneas pueden dejar el servidor prácticamente paralizado
- Django procesa las solicitudes
multipart/form-dataconMultiPartParser, y como el middleware CSRF accede arequest.POSTantes de entrar a la view, este parser siempre se ejecuta incluso sin autenticación - El núcleo de la vulnerabilidad está en una estructura donde se multiplican tres capas
- (Layer 1) while-loop de alineación base64: si al eliminar espacios en blanco del chunk el estado
remaining != 0se mantiene,field_stream.read(1)se sigue llamando repetidamente sobre el resto completo del stream - (Layer 2) costo oculto O(C) de
LazyStream.read(1): cada llamada aread(1)internamente extrae un buffer completo de ~64 KB y luego vuelve a insertar 65,535 bytes conunget(), repitiendo ese patrón - (Layer 3) concatenación O(C) de bytes en
unget(): cada vez se crea un objeto nuevo conbytes + self._leftover
- (Layer 1) while-loop de alineación base64: si al eliminar espacios en blanco del chunk el estado
- Una sola solicitud de 2.5 MB provoca internamente alrededor de 86 GB de copias de memoria, y en un M2 ocupa por completo un worker durante unos 5.3 segundos. Con 20 MB, tarda cerca de 1 minuto
- Dentro de
unget()ya existía código de sanity check (_update_unget_history), pero este ataque usa un patrón monótonamente decreciente donde el tamaño deunget()baja en 1 en cada llamada, por lo que nunca cumple la condición de detección (number_equal > 40) - El núcleo del parche del equipo de Django cambia
read(4 - remaining)porread(self._chunk_size), para leer 64 KB de una vez en lugar de 1 a 3 bytes. Con eso, las llamadas a read bajan de 2.5 millones a unas 40 - El valor predeterminado de
client_max_body_sizeen Nginx es 1 MB, pero suele relajarse en endpoints de carga de archivos, y el valor predeterminado deLimitRequestBodyen Apache httpd es 1 GB, así que solo con el proxy no se garantiza la defensa - La vulnerabilidad fue descubierta usando Claude Code + Codex, y resulta llamativo que un framework refinado durante casi 20 años aún conservara un DoS preautenticación
4 comentarios
Vamosss
¿Alguien ya probó hacerlo directamente?
Hay un PoC hecho para demostración en GitHub.
https://github.com/ch4n3-yoon/CVE-2026-33033-PoC
Está buenísimo.