Engineering

Django 6.1.2 and DRF 3.18.3 Security Releases: How to Upgrade

On this page
  1. What to install
  2. The header parsing bug every API should fix
  3. What behaves differently afterwards
  4. Language codes in LocaleMiddleware
  5. Model formsets with editable primary keys
  6. GeoDjango raster lookups
  7. How to upgrade
  8. Expect the next one soon
  9. Frequently asked questions

On October 6, 2026 Django released 6.1.2, 6.0.9 and 5.2.18 with fixes for four CVEs. The same day, Django REST Framework published a high-severity advisory for the same kind of bug in its own content negotiation. If you run a REST Framework API, you need both upgrades. This post explains which fixes reach a typical API, what behaves differently after the upgrade, and how to prove in a test that you are patched.

What to install#

You runUpgrade to
Django 6.16.1.2
Django 6.06.0.9 (extended support until April 2027)
Django 5.2 LTS5.2.18 (supported until April 2028)
Django 5.1 or olderNo fix. Move to 5.2 LTS or 6.1
Django REST Framework3.18.3, not 3.18.2

REST Framework 3.18.2 carried the security fixes but also changed order_by_precedence by mistake, and 3.18.3 reverts that. Go straight to 3.18.3. It supports Django 5.2, 6.0 and 6.1.

The header parsing bug every API should fix#

CVE-2026-84429 (moderate) is in django.utils.http.parse_header_parameters(), the function that splits a header such as Content-Type: multipart/form-data; boundary=... into a type and its parameters. A quoted parameter full of separators made it take quadratic time. Django calls it for the Content-Type of every request, for the Accept header in request.accepts(), and for every part of a multipart upload. No login is needed to send those headers.

Django itself reads Accept in only a couple of places. REST Framework reads it on every request, because content negotiation decides between JSON, the browsable API and any other renderer you configured. Its media type class calls the same function once per entry in the header:

rest_framework/utils/mediatypes.py
from django.utils.http import parse_header_parameters
...
self.full_type, self.params = parse_header_parameters(self.orig)

REST Framework's advisory GHSA-33wh-fxxf-88vv describes an Accept header with many large quoted parameters that burns about 19 seconds of CPU per request. A handful of such requests ties up every worker. Version 3.18.2 caps the header at 8,192 bytes, 64 entries and 256 characters per media type, and parses each entry once.

I timed the same growing Accept header against both pairs of versions. On Django 6.1.1 with REST Framework 3.18.1, the request time grew roughly with the square of the header size, and it was already many times slower than normal at 8 KB. That is why a proxy's usual 8 KB header limit is not enough protection. On Django 6.1.2 with REST Framework 3.18.3, every oversized header was answered in under a millisecond.

What behaves differently afterwards#

Django now parses header parameters with Python's email.message.Message. I compared 6.1.1 and 6.1.2 side by side, and these are the differences you may notice:

InputDjango 6.1.1Django 6.1.2
text/html; foo (bare parameter)dropped: {}kept: {'foo': ''}
RFC 2231 value with no encodingValueErrordecoded
RFC 2231 value with an unknown charset in Content-TypeacceptedBadRequest while the request is built
Accept entry over 256 characters, with DRF 3.18.3200406 Not Acceptable

Real browsers and HTTP clients send none of these, so most projects see no change. If you have unusual clients, such as an old device that sends long or odd headers, test against them before you deploy.

Language codes in LocaleMiddleware#

CVE-2026-77050 (low) is in get_supported_language_variant(). Language codes were cached before their length was checked, so an endless supply of long, unique codes could fill process memory. Codes over 500 characters are now rejected or truncated first.

You are exposed if LocaleMiddleware is in MIDDLEWARE and USE_I18N is on. Reading the 6.1.2 source, the middleware passes the language cookie and the first segment of the URL path to this function on every request, whether or not you use i18n_patterns. A new project from startproject does not include LocaleMiddleware, so check your settings rather than assuming.

One change the release notes do not mention: the cache moved to a private helper, so get_supported_language_variant.cache_clear() now raises AttributeError. If your tests call it to reset translations between cases, they break after the upgrade. I confirmed this on 6.1.2.

Model formsets with editable primary keys#

CVE-2026-87975 (moderate) affects model formsets, which the Django admin uses for inlines. With forged POST data, an attacker could delete objects outside the formset's queryset or create objects through an edit_only formset. It needs a primary key that the form can set: a OneToOneField used as the primary key, a parent link in multi-table inheritance, or a natural or UUID key included in the form's fields. Models with the default BigAutoField are not affected.

Pure REST Framework APIs do not use formsets, but most projects also run the admin. If any admin inline uses a model with one of those primary keys, this fix matters to you.

GeoDjango raster lookups#

CVE-2026-87890 (moderate) is for GeoDjango users. Raster values passed to a spatial lookup as raw bytes could contain a GDAL VRT document that made the server send network requests. This is a gap left by the August fix for CVE-2026-15307. Raw raster bytes are now refused with DisallowedRasterLookup; wrap them in GDALRaster if you really mean to pass a raster:

parcels/queries.py
from django.contrib.gis.gdal import GDALRaster

# Before 6.1.2, raw bytes were accepted as a raster lookup value
Parcel.objects.filter(area__intersects=GDALRaster(raster_bytes))

Bytes that are a valid hex geometry are still accepted as before. The release also tightens an earlier fix for deeply nested geometry collections: max_geom_collections (default 198) now limits nesting depth for WKB input too.

How to upgrade#

  1. Raise the pins in your requirements file and install.
  2. Run your whole test suite, then add the regression test below.
  3. Deploy, and check the error tracker for new BadRequest or 406 responses over the next day.
requirements.txt
Django>=6.1.2,<6.2
djangorestframework>=3.18.3
bash
pip install -r requirements.txt
python -c "import django, rest_framework; print(django.get_version(), rest_framework.VERSION)"
pip-audit

This test passes only when the REST Framework caps are in place, so it fails loudly if someone pins an old version again. It sends a harmless but oversized Accept header; I checked that it returns 200 on REST Framework 3.18.1 and 406 on 3.18.3. See testing REST Framework APIs with pytest for the fixtures.

tests/test_security_headers.py
import pytest
from rest_framework.test import APIClient

@pytest.mark.django_db
def test_oversized_accept_header_is_rejected():
    client = APIClient()
    oversized = 'application/json; note="' + "a" * 300 + '"'

    response = client.get("/api/posts/", HTTP_ACCEPT=oversized)
    assert response.status_code == 406

    normal = client.get("/api/posts/", HTTP_ACCEPT="application/json")
    assert normal.status_code == 200

Expect the next one soon#

Django has shipped security releases in eight months of 2026 so far: February through August and now October. The previous one, on August 4, fixed four CVEs including a high-severity one in spatial lookups. Treat these as routine. Let Dependabot or Renovate open the pull request, keep a test suite that runs in minutes, and subscribe to the django-announce mailing list for the advance notice the security team sends a week before each release.

If you are also planning the move to Python 3.15, wait for the Django release that officially supports it. No Django version does yet, although REST Framework 3.18.2 and later already declare it.

Frequently asked questions#

Which Django versions fix the October 2026 security issues?

Django 6.1.2, 6.0.9 and 5.2.18, released on October 6, 2026. Older series such as 5.1 and 4.2 get no fix; upgrade to 5.2 LTS or 6.1.

Do I need to upgrade Django REST Framework too?

Yes. REST Framework parses the Accept header with the affected Django function and published its own high-severity advisory, GHSA-33wh-fxxf-88vv. Install 3.18.3, which includes the fix and reverts a mistake in 3.18.2.

Why does my API now return 406 for some requests?

REST Framework 3.18.2 and later reject an Accept header over 8,192 bytes, with more than 64 media types, or with a media type longer than 256 characters. Normal clients never hit these limits.

Am I affected by the language code issue if I do not use i18n_patterns?

Yes, if LocaleMiddleware is enabled with USE_I18N on. The middleware passes the language cookie and the first URL segment to the affected function on every request.

Comments

No comments yet. Be the first.

Leave a comment

Only used to tell you about a reply. Never shown.

Plain text; line breaks are kept.

Let's build something

Hiring for a backend role, or have a project in mind? Send a message and I will reply by email.

At most 2 links.