---
title: "Django 6.1.2 and DRF 3.18.3 Security Releases: How to Upgrade"
url: "https://nareshkumar.online/blog/django-6-1-2-security-release"
author: "Naresh Kumar"
published: "2026-10-10"
updated: "2026-10-10"
category: "Engineering"
tags: ["Django", "Django REST Framework", "Python", "Security"]
---

# Django 6.1.2 and DRF 3.18.3 Security Releases: How to Upgrade

**In short:** Upgrade to Django 6.1.2, 6.0.9 or 5.2.18 and to Django REST Framework 3.18.3. The header-parsing fix is the one every API needs: an unauthenticated Accept or Content-Type header could burn CPU on each request. Check the small behavior changes listed below before you deploy.

On October 6, 2026 Django released [6.1.2, 6.0.9 and 5.2.18](https://www.djangoproject.com/weblog/2026/oct/06/security-releases/) 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 run | Upgrade to |
| --- | --- |
| Django 6.1 | 6.1.2 |
| Django 6.0 | 6.0.9 (extended support until April 2027) |
| Django 5.2 LTS | 5.2.18 (supported until April 2028) |
| Django 5.1 or older | No fix. Move to 5.2 LTS or 6.1 |
| Django REST Framework | 3.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](https://github.com/encode/django-rest-framework/releases/tag/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`

```python
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](https://github.com/encode/django-rest-framework/security/advisories/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.

> **Warning:** ⚠️ The advisory lists versions up to 3.18.0 as affected, but 3.18.1 does not have the caps either. If you are on 3.18.1, upgrade too.

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:

| Input | Django 6.1.1 | Django 6.1.2 |
| --- | --- | --- |
| `text/html; foo` (bare parameter) | dropped: `{}` | kept: `{'foo': ''}` |
| RFC 2231 value with no encoding | `ValueError` | decoded |
| RFC 2231 value with an unknown charset in `Content-Type` | accepted | `BadRequest` while the request is built |
| `Accept` entry over 256 characters, with DRF 3.18.3 | 200 | 406 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`

```python
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`

```text
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](https://nareshkumar.online/blog/testing-drf-apis-with-pytest) for the fixtures.

`tests/test_security_headers.py`

```python
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](https://nareshkumar.online/blog/python-3-15-django-developers), 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.
