Django 6.1.3 Fixes MultiPartParser Memory and GeoJSON SSRF

Django 6.1.3, 6.0.10, and 5.2.19 fix a MultiPartParser memory exhaustion bug and a GeoJSON SSRF vulnerability. Here is the operational audit for web teams.

The Django project released security updates on November 3, 2026, addressing six vulnerabilities across its current release series. The updates arrive as Django 6.1.3, Django 6.0.10, and Django 5.2.19 on the long term support track. The official announcement on the Django Project Weblog outlines patches for memory exhaustion in file uploads, server-side request forgery through geospatial inputs, log injection, administrative password hash exposure, and cache header mishandling.

Web development teams running Django services in production should review their configurations immediately. While several items in this advisory target the GeoDjango extension library, the most widespread operational risk stems from a flaw in standard multipart request processing. Any public endpoint that accepts file uploads, including standard form views and API endpoints using multi-part parsers, can face worker memory exhaustion regardless of whether the application uses spatial features. This audit covers what changed in the request pipeline, how the vulnerabilities operate, and how engineering teams can verify their defenses.

Why the Django 6.1.3 update matters for production deployments

Security releases in the Django ecosystem follow a coordinated monthly cadence. The November 3 batch comes four weeks after the October update detailed in our coverage of Django 6.1.2 and its formset patches. While that earlier cycle focused on quadratic time complexity during header parsing and model formset primary key manipulation, the November release shifts attention toward lower-level parser behaviors and C-library bindings.

The six tracked vulnerabilities in this cycle include:

  • CVE-2026-108147: Potential memory exhaustion in MultiPartParser via base64 encoded file uploads.
  • CVE-2026-111362: Server-side request forgery through the coordinate reference system member of GeoJSON geometries.
  • CVE-2026-111211: Log injection through unescaped geometry form inputs.
  • CVE-2026-113733: Password hash disclosure in administrative change forms for read-only staff accounts.
  • CVE-2026-113593: Potential exposure of private data through empty and missing Vary headers in cache middleware.
  • CVE-2026-114582: Server-side request forgery through spatial reference string inputs in GDAL geometry transformations.

Production teams often make the mistake of deprioritizing Django security announcements when the advisory headline mentions spatial libraries. Because GeoDjango requires system dependencies like GDAL and GEOS that many basic CRUD applications do not install, developers assume their workloads are immune. CVE-2026-108147 disproves that assumption. It lives in the core HTTP handling code of django.http.multipartparser.MultiPartParser, which ships with every standard Django installation and runs whenever a view inspects request.FILES.

The update also reinforces how upstream dependencies interact with Python web frameworks. When a Python library acts as a wrapper around an established C or C++ library, security assumptions between the two layers frequently diverge. The GeoDjango patches in Django 6.1.3 illustrate that gap directly, requiring the Python layer to place strict boundaries on input strings before passing them to native drivers.

Memory exhaustion in MultiPartParser under base64 transfers

Standard file upload processing in Django relies on a pipeline of upload handlers configured through FILE_UPLOAD_HANDLERS. By default, Django uses MemoryFileUploadHandler followed by TemporaryFileUploadHandler. The threshold between these two handlers is controlled by FILE_UPLOAD_MAX_MEMORY_SIZE, which defaults to 2,621,440 bytes (2.5 megabytes). Uploads smaller than this threshold remain in system memory as InMemoryUploadedFile objects. Uploads that exceed this limit spill over to temporary disk storage as TemporaryUploadedFile objects, protecting the application process from exhausting available RAM.

Ignacy Kujawa identified that this boundary broke down when incoming requests used multipart boundaries containing the header Content-Transfer-Encoding: base64. While modern HTTP clients rarely use base64 transfer encoding for multipart bodies, RFC specifications historically permitted it. Django retained compatibility code to parse and decode base64 streams before handing individual payload chunks over to the configured upload handler chain.

In affected versions across the 6.1, 6.0, and 5.2 branches, MultiPartParser attempted to read the entire base64-encoded stream into a single internal buffer to decode it into raw bytes. This initial ingestion step occurred before the upload handlers received chunks and before FILE_UPLOAD_MAX_MEMORY_SIZE could evaluate the payload size. An external client could transmit a large multipart stream with base64 encoding headers, forcing the Python process to allocate hundreds of megabytes of heap memory in a single request.

In containerized deployments on platforms like Kubernetes or AWS ECS, worker processes run under strict memory limits. When a worker process exceeds its memory ceiling, the operating system kernel sends a SIGKILL through the out-of-memory killer. A single client connection transmitting a continuous base64 stream could take down worker processes sequentially, causing service degradation or cascading 502 Bad Gateway errors across a cluster.

The patch in Django 6.1.3 rewrites the base64 decoding logic within MultiPartParser to stream through a bounded sliding buffer. The parser now decodes incoming chunks incrementally and passes them to the configured upload handlers as they arrive. If the cumulative byte count crosses FILE_UPLOAD_MAX_MEMORY_SIZE, the upload handlers correctly redirect the stream into temporary disk files. This preserves the memory ceiling regardless of the transfer encoding header used in the multipart boundary.

The GeoJSON coordinate reference system SSRF vector

The most severe vulnerability addressed in this cycle is CVE-2026-111362, carrying a moderate severity rating. The flaw resides in django.contrib.gis.gdal.OGRGeometry and affects applications that parse user-supplied GeoJSON representations into spatial objects.

GeoJSON is a standard format for encoding geographic data structures. In older revisions of the GeoJSON specification, geometry objects could include an optional top-level crs member specifying the coordinate reference system. That specification allowed the crs property to reference a Coordinate Reference System via a linked uniform resource identifier (URI). When GeoDjango constructs an OGRGeometry or a GEOSGeometry from GeoJSON, it delegates the parsing work to the underlying GDAL (Geospatial Data Abstraction Library) C-library.

GDAL contains built-in network and filesystem resolution routines. When GDAL encountered a linked CRS URI in a GeoJSON document, its default behavior was to resolve the external link by making an outbound HTTP request or reading a local file path. Because Django’s spatial fields, forms, and lookups passed user-supplied GeoJSON strings directly to GDAL routines without inspecting the crs structure, an external user could supply a crafted GeoJSON payload containing a URI pointing to internal infrastructure.

This created two distinct security hazards:

  1. Server-side request forgery (SSRF): The Django application process would initiate outbound network requests to internal addresses, such as the cloud instance metadata service at 169.254.169.254 or private internal microservices inaccessible from the public internet.
  2. Arbitrary local file disclosure: If the URI pointed to a local filesystem path, GDAL attempted to open the file and parse it as a spatial reference definition. Error messages or debugging responses could expose file contents or confirm the existence of specific files on the application host.

A companion issue, tracked as CVE-2026-114582, affected SpatialReference string inputs passed directly to transformation helpers. Methods such as GEOSGeometry.transform(), OGRGeometry.transform(), and GDALRaster.transform() permitted arbitrary string inputs that triggered identical GDAL network lookup behaviors.

Django 6.1.3, 6.0.10, and 5.2.19 fix this vulnerability by introducing validation in django.contrib.gis.forms.GeometryField, GEOSGeometry, and spatial lookup filters. GeoDjango now explicitly inspects incoming GeoJSON objects and rejects payloads that contain external resource links within the crs definition. Applications that need to load spatial data from external URLs can still do so through trusted administrative utilities such as DataSource and LayerMapping, where input is controlled by site operators rather than end users.

Additionally, the Django release introduces a new deployment check, gis.W001. This check evaluates the installed version of the host GDAL library and warns administrators if the system uses GDAL versions older than 3.9, where upstream URL resolution quirks are most prevalent.

Administrative read-only hash leaks and cache Vary inconsistencies

The remaining three CVEs address privilege separation in the Django administrative interface, cache isolation in middleware, and input sanitization in logging handlers.

Password hash exposure in the admin interface

Under CVE-2026-113733, the Django admin interface exhibited an information leak when displaying custom user models. In standard Django configurations, the built-in User model masks password hashes, showing a status indicator and a link to a separate password change form rather than the raw PBKDF2, Argon2, or bcrypt hash string.

However, when an application implemented a custom user model outside the primary AUTH_USER_MODEL declaration, or utilized multi-table model inheritance to extend user profiles, read-only change forms did not apply this masking logic. A staff user with read-only permissions (view permission without change permission) on the custom model could view the full cryptographic password hash in the rendered form HTML. While modern password hashes are salted and stretched, exposing raw hash strings provides attackers with offline cracking opportunities.

Django 6.1.3 and 6.0.10 ensure that read-only admin forms uniformly mask password fields across all models that define password attributes, preventing staff users from retrieving stored hash representations regardless of inheritance patterns. The official advisory lists patches for this item on the 6.1 and 6.0 lines (and main); the 5.2.19 LTS package in the same batch covers the other five issues.

Cache poisoning through missing Vary headers

Under CVE-2026-113593, a logic inconsistency in django.middleware.cache.UpdateCacheMiddleware and the cache_page() decorator could lead to private data exposure. The HTTP Vary header instructs downstream caches and internal cache middleware which request headers must match before a cached response can be served to a subsequent client.

When generating cache keys, Django’s caching subsystem treated a missing Vary header and an empty Vary header (Vary: "") as functionally identical. In certain proxy and reverse proxy configurations, responses containing empty Vary values could overwrite cache entries that expected standard header variations, such as Accept-Language or Cookie. This created edge cases where authenticated pages or user-specific content could be returned to unauthenticated clients. The patch separates the internal handling of absent and empty Vary headers, guaranteeing strict key isolation.

Log injection in geometry form fields

Tracked as CVE-2026-111211 and reported by Aaron chen, django.contrib.gis.forms.GeometryField and its associated form widgets logged invalid geometry input strings directly to application loggers without escaping control characters. An attacker submitting input containing carriage returns (\r), newlines (\n), or terminal ANSI escape codes could inject fake log entries or corrupt log file parsing. If system logs were viewed in a terminal emulator or ingested by log analytics pipelines that rely on newline delimiters, an adversary could spoof log events to disguise malicious actions. Django 6.1.3 strips control characters from rejected geometry strings before sending them to logging backends.

Reverse proxy boundaries and defense in depth for Python web applications

The vulnerabilities patched in Django 6.1.3 provide a useful case study in defense in depth. Python web applications do not operate in a vacuum; they sit behind reverse proxies, load balancers, and container orchestration layers. Relying solely on the application framework to catch abnormal request payloads leaves servers vulnerable when framework parsers encounter unexpected input.

Consider the memory exhaustion flaw in MultiPartParser (CVE-2026-108147). While Django has now patched the issue by streaming base64 decoding through chunked buffers, an application should never be the first layer to inspect an oversized request body. Reverse proxies like Nginx, Caddy, or Envoy must enforce rigid body limits before traffic reaches Python worker processes like Gunicorn or Uvicorn.

For example, in an Nginx configuration terminating client connections before forwarding traffic to Django:

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # Enforce global body limit matching application business requirements
    client_max_body_size 10M;

    # Buffer client request bodies to disk if they exceed this memory threshold
    client_body_buffer_size 128k;

    location /api/v1/uploads/ {
        # Strict limit for dedicated upload endpoints
        client_max_body_size 25M;
        proxy_pass http://django_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location / {
        # Restrict general endpoints that do not accept file payloads
        client_max_body_size 1M;
        proxy_pass http://django_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

When an upstream reverse proxy sets client_max_body_size 10M, an incoming request containing a 500 MB base64 multipart stream is rejected at the Nginx edge with an HTTP 413 Request Entity Too Large. The request never reaches Gunicorn or Django, preventing the Python runtime from allocating memory regardless of parser bugs. This architecture mirrors the proxy boundary considerations discussed in our analysis of Starlette Host header validation and ASGI proxies, where network edges must enforce protocol boundaries before passing requests downstream.

A similar defense in depth principle applies to the GeoJSON SSRF vulnerability (CVE-2026-111362). Because GDAL is written in C++ and links against network libraries like libcurl, allowing the application container to make unrestricted outbound connections is an unnecessary risk. In modern container environments, web workers handling user requests should operate under strict egress firewall rules:

Egress Traffic Rules for Django Web Workers:
  - Allow: Database connection (TCP 5432 to internal RDS/PostgreSQL)
  - Allow: Cache cluster (TCP 6379 to internal Redis)
  - Allow: Object storage (HTTPS 443 to S3 endpoint / VPC endpoint)
  - Deny:  All other outbound TCP/UDP traffic, including metadata IPs (169.254.169.254)

By applying egress network policies at the Kubernetes namespace or AWS security group level, even if an application dependency attempts an unauthorized URL lookup, the network layer drops the packet immediately. The SSRF attack fails regardless of the application framework state.

Verification steps and upgrade playbook for maintainers

Upgrading to the patched versions requires minimal code adjustment because the security releases maintain backward compatibility. Teams should follow a structured deployment and verification process.

Step 1: Update dependency pins

Depending on your package management tool, update your dependency definitions to pin the appropriate patch version:

For projects using pyproject.toml or requirements.txt:

# For projects on the 6.1 branch
Django>=6.1.3,<6.2

# For projects on the 6.0 branch
Django>=6.0.10,<6.1

# For projects on the 5.2 LTS branch
Django>=5.2.19,<5.3

When managing Docker image rebuilds in CI/CD pipelines, ensure your build steps invalidate cached dependency layers as outlined in our guide on pip dependency caching for Docker web deployments. Rebuilding images without busting package cache layers can leave old wheels active in your container registry.

Step 2: Run Django system deployment checks

Once updated, run Django’s built-in deployment validator against your settings configuration:

python manage.py check --deploy

Review the output specifically for the new geospatial check gis.W001. If your application uses GeoDjango and the underlying system environment runs GDAL older than 3.9, the check will emit a warning:

SystemCheckError: System check identified some issues:

WARNINGS:
?: (gis.W001) The installed GDAL library version (3.8.4) is older than 3.9.0.
    HINT: Upgrade GDAL on your host or container base image to avoid known reference resolution issues.

If this warning appears, update the base operating system packages in your Dockerfile. On Debian 13 (Trixie) or modern Alpine images, ensure packages like gdal-bin and libgdal-dev pull the latest available security updates.

Step 3: Review custom upload handlers and settings

Inspect your application’s settings file for upload parameters:

# settings.py audit

# Verify that FILE_UPLOAD_MAX_MEMORY_SIZE is explicitly set or using the default 2.5 MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440  # 2.5 MB

# Ensure FILE_UPLOAD_HANDLERS contains standard streaming handlers
FILE_UPLOAD_HANDLERS = [
    "django.core.files.uploadhandler.MemoryFileUploadHandler",
    "django.core.files.uploadhandler.TemporaryFileUploadHandler",
]

# Ensure custom user models properly handle password fields in admin
# Custom admin classes should use UserAdmin or inherit SensitivePostParametersMixin

If your application implements custom upload handlers by subclassing FileUploadHandler, verify that your receive_data_chunk() implementations do not accumulate raw chunks into memory variables without size checks. Custom handlers should process chunks in a streaming manner, writing directly to disk or streaming to external storage backends.

Step 4: Audit GeoJSON input validation

For applications accepting user-submitted geospatial data through Django REST Framework, Ninja, or standard form views, verify that geometry fields use official serializers or form fields rather than raw string parsing.

# Example form view using validated GeometryField
from django import forms
from django.contrib.gis import forms as gis_forms

class ZoneSubmissionForm(forms.Form):
    name = forms.CharField(max_length=100)
    # Using gis_forms.GeometryField ensures Django 6.1.3 CRS rejection logic runs
    boundary = gis_forms.GeometryField(widget=gis_forms.OSMWidget)

    def clean_boundary(self):
        geom = self.cleaned_data["boundary"]
        # In Django 6.1.3, any external CRS reference in the GeoJSON input
        # is rejected during initial field validation before reaching this method
        return geom

Avoid passing raw GeoJSON strings directly to low-level GDAL wrappers like OGRGeometry(raw_json_string) without prior validation. Utilizing GeometryField or converting inputs through GEOSGeometry in Django 6.1.3 guarantees that the framework rejects external CRS references before native GDAL calls occur.

The November 3 updates for Django 6.1.3, 6.0.10, and 5.2.19 address practical vulnerabilities across both core HTTP infrastructure and specialized spatial extensions. Upgrading takes only a few minutes, eliminates critical memory exhaustion and SSRF attack vectors, and ensures production services remain stable under hostile input conditions.

Spread The Article

Share this guide

Send this article to your network or keep a copy of the direct link.

X Facebook LinkedIn Reddit Telegram

Discussion

Leave a comment

No comments yet

Be the first to start the conversation.