---
django: main at 4fab678a0739d54401ccee7eb587553657c9f76e (2026-09-26), on its way to 6.2
---

# Security fixes: limits against denial of service

The security fixes from 5.0 to this commit that bound the work Django does on what a client sends: `strip_tags`, `Truncator`, `wrap`, `intcomma`, `floatformat` and the URL linker; URLs and redirects; IPv6 addresses and language codes; headers and bodies; the XML deserializer; nested geometry collections. For each, what was open, the limit at this commit, and the releases that brought it.

A denial of service of this kind needs no wrong answer, only an input that costs far more than it should: work quadratic in its length, a string copied again at each step, a regular expression that backtracks, a body read past its limit, a cache of keys of any length, a C library that crashes on deep nesting. Most of the fixes take one of two shapes. Some bound the input, by a length or a count past which it is refused, cut short or passed through untouched. Others make the work linear: a list joined once in place of a string grown, a stack popped only at its top. A refusal uses an exception Django already has: `SuspiciousOperation`, a 400 when a view lets it escape (`django/core/handlers/exception.py:response_for_exception`), `ValidationError`, `LookupError`, `ValueError`, or a false answer from a check.

## Markup and text

`strip_tags` feeds the text to an HTML parser, `MLStripper`, and repeats while a pass still removes a tag (`django/utils/html.py:strip_tags`). Long runs of nested incomplete entities made it extremely slow (CVE-2024-53907, 5.0.10 and 5.1.4): the loop is now allowed `MAX_STRIP_TAGS_DEPTH` passes, 50, and raises `SuspiciousOperation` if the text still holds a "<" and a ">" after them. Many unclosed opening tags made it slow too (CVE-2025-32873, 5.1.9 and 5.2.1): before any pass, an opening tag that runs a thousand characters without a ">" and holds 50 "<" or more raises the same. So does the `striptags` filter, which calls it.

`Truncator.words` with html=True, behind `truncatewords_html`, used a regular expression that a crafted string could make backtrack (CVE-2024-27351, 5.0.3). Since 5.1 both HTML methods parse with subclasses of `html.parser.HTMLParser` instead ([What changed in time zones, translation and the utilities](i18n-and-utils.md)), and that parsing was quadratic on many unmatched end tags (CVE-2026-1285, 5.2.11 and 6.0.2). An end tag now pops the stack of open tags only when it closes the tag opened last; the comment there says this avoids a linear scan for each unmatched one (`django/utils/text.py:TruncateHTMLParser.handle_endtag`).

`wrap`, behind `wordwrap`, was open to very long strings (CVE-2025-26699, 5.0.13 and 5.1.7). At this commit it hands each line to `textwrap.TextWrapper` (`django/utils/text.py:wrap`; [Text helpers: `Truncator` and `django/utils/text.py`](../utils/text.md)).

## Numbers: `intcomma` and `floatformat`

`intcomma` was open to very long strings (CVE-2024-24680, 5.0.2). With its use_l10n argument true, the default, a value that can be made a decimal goes to `number_format`; otherwise the leading digits of its text are grouped by one substitution over them reversed, linear in their length (`django/contrib/humanize/templatetags/humanize.py:intcomma`; [Humanize: `naturaltime`, `intcomma` and the other filters](../contrib/humanize.md)).

`floatformat` could use much memory on a number in scientific notation with a large exponent (CVE-2024-41989, 5.0.8). It now returns its input as given when the decimal's digits and the absolute value of its exponent come to more than 200: `"1e200"` comes back as `"1e200"`, and `"1e199"` is written out in its 200 digits (`django/template/defaultfilters.py:floatformat`; [The built-in filters](../templates/filters.md)).

## URLs and Unicode on Windows

Splitting a URL with `urllib.parse.urlsplit` involves Unicode's NFKC normalization, which the notes of several fixes say is slow on Windows. Several guards here stop a long URL before it is split, at one bound: `MAX_URL_LENGTH`, 2,048 characters (`django/utils/http.py`).

The URL linker, `django.utils.html.urlize` and the `urlize` and `urlizetrunc` filters (`django/utils/html.py:Urlizer`), had fixes in 2024: for input with very many brackets (CVE-2024-38875, 5.0.7), very large input with a particular sequence of characters (CVE-2024-41990, 5.0.8; CVE-2024-45230, 5.0.9 and 5.1.1), and input with very many Unicode characters, which reached the admin's URL widget too (CVE-2024-41991, 5.0.8). At this commit a word whose text, once its punctuation is trimmed, is longer than `MAX_URL_LENGTH` is not tried as a URL (`django/utils/html.py:Urlizer.handle_word`), and trimming a word's punctuation is linear: each kind of bracket is counted once, in a `CountsDict`, the trimmed pieces are joined once, and only the text from the last "&" is unescaped to judge a trailing ";" (`django/utils/html.py:Urlizer.trim_punctuation`). `AdminURLFieldWidget` links its value only when its validator, by default `URLValidator`, accepts it, and the validator refuses a URL over `URLValidator.max_length`, `MAX_URL_LENGTH`, before it splits it (`django/contrib/admin/widgets.py:AdminURLFieldWidget.get_context`).

`LoginView`, `LogoutView` and `set_language` were open to input with very many Unicode characters on Windows (CVE-2025-27556, 5.0.14 and 5.1.8). Each judges the URL it is to redirect to with `url_has_allowed_host_and_scheme`, which refuses one over `MAX_URL_LENGTH` before splitting it (`django/utils/http.py:_url_has_allowed_host_and_scheme`; [The helpers in `django/utils/http.py`](../http/helpers.md#urls-that-came-from-a-client)). The redirects followed (CVE-2025-64458, 5.1.14 and 5.2.8): `HttpResponseRedirectBase.__init__`, and so `redirect`, raises `DisallowedRedirect` for a `"Location"` longer than `MAX_URL_REDIRECT_LENGTH`, 16,384 characters, before it splits the target (`django/http/response.py:HttpResponseRedirectBase.__init__`; [The response object](../http/response.md#redirects)). Since 6.1 a caller may pass another max_length ([What changed in handlers, requests, URLs and views](request-cycle.md)).

The form field's `URLField.to_python` split the value to decide whether to put a scheme in front (CVE-2026-25673, 5.2.12 and 6.0.3). It now looks only at what precedes the first colon, and never calls `urllib.parse.urlsplit` (`django/forms/fields.py:URLField.to_python`). Control characters inside a value are left to the validators, and `URLValidator` refuses a tab, a carriage return, a newline and a null ([The built-in form fields](../forms/field-types.md#urls-and-the-assumed-scheme)).

## Addresses and language codes

IPv6 validation had no upper bound on the length of its input (CVE-2024-56374, 5.0.11 and 5.1.5). `is_valid_ipv6_address` now refuses text longer than `MAX_IPV6_ADDRESS_LENGTH`, 39 characters, eight groups of four and their colons, before `ipaddress` parses it, and `clean_ipv6_address` text longer than its max_length, by default the same (`django/utils/ipv6.py:_ipv6_address_from_str`). `django.forms.GenericIPAddressField` takes 39 as its max_length unless given another, and passes it to `clean_ipv6_address` (`django/forms/fields.py:GenericIPAddressField`). The model field, the notes say, was not affected ([Dates, durations and addresses: `parse_datetime`, `parse_duration` and `clean_ipv6_address`](../utils/parsing.md#ipv6-addresses-clean_ipv6_address)).

`get_supported_language_variant` was open to very long codes holding particular characters (CVE-2024-39614, 5.0.7). It now cuts a code longer than `LANGUAGE_CODE_MAX_LENGTH`, 500 characters, at its last hyphen within the limit, and refuses it with `LookupError` if it has none there or strict is true (`django/utils/translation/trans_real.py:get_supported_language_variant`). The 5.0.7 notes say `ValueError`; the code raises the `LookupError` its callers catch, and 5.0.8 fixed a crash in `LocaleMiddleware` on such a code (ticket 35627). `check_for_language` kept each code it was asked about as a key of its cache, bounded in entries but not in their length (CVE-2026-15337, 5.2.17 and 6.0.8). At this commit it is not cached: it refuses a code over the limit, and asks the cached `translation_catalog_exists`, which holds 1,000 codes, about the rest (`django/utils/translation/trans_real.py:check_for_language`; [Choosing a language: `LocaleMiddleware`](../i18n/choosing.md#supported-codes)).

## Headers and bodies

Under ASGI a header sent many times was combined by repeated string concatenation (CVE-2025-14550, 5.2.11 and 6.0.2). `ASGIRequest.__init__` now gathers each header's values in a list and joins them once, with commas, or for `"cookie"` with a semicolon and a space (`django/core/handlers/asgi.py:ASGIRequest.__init__`; [Making the request: `WSGIRequest` and `ASGIRequest`](../http/request.md#from-an-asgi-scope)).

An ASGI request with no Content-Length, or one smaller than the body, could take a body past `DATA_UPLOAD_MAX_MEMORY_SIZE` into `HttpRequest.body` (CVE-2026-33034, 5.2.13 and 6.0.4), or keep an upload in memory past `FILE_UPLOAD_MAX_MEMORY_SIZE` (CVE-2026-5766, 5.2.14 and 6.0.5). `body` refuses a declared length over the limit before reading, and then, when the stream can seek, as an ASGI request's can, measures it and raises `RequestDataTooBig` if it is over (`django/http/request.py:HttpRequest.body`; [The body: `body`, `POST` and `FILES`](../http/body.md#under-asgi)). `MemoryFileUploadHandler.handle_raw_input` decides whether files stay in memory by the size of a stream that can seek; a WSGI request's `LimitedStream` cannot, and there the header, which it enforces, still decides (`django/core/files/uploadhandler.py:MemoryFileUploadHandler.handle_raw_input`; [Multipart parsing and upload handlers](../http/uploads.md#memory-or-disk)).

A file sent in Base64 with much white space could make the multipart parser copy memory again and again (CVE-2026-33033, 5.2.13 and 6.0.4). At this commit the parser strips a chunk's white space, reads further chunks whole until the stripped length is a multiple of four, and joins the pieces once before decoding (`django/http/multipartparser.py:MultiPartParser._parse`).

## The XML deserializer

Deserializing XML took quadratic time on many nested invalid elements, as `getInnerText` gathered text inefficiently as it recursed (CVE-2025-64460, 5.1.15 and 5.2.9); the fix had it collect text element by element. Since 6.1 it reads only a node's own text, and `check_element_type` raises `SuspiciousOperation` for a child element with content of its own, so a nested element with content is refused, not walked (`django/core/serializers/xml_serializer.py:check_element_type`; [What changed in caching, files, signals, serialization and pagination](caching-files-signals.md)).

## Nested geometry collections

Deeply nested geometry collections crashed GEOS, through `GEOSGeometry`, spatial lookups and the geometry form field (CVE-2026-15830, 5.2.17 and 6.0.8). Django now counts before GEOS reads: in WKT, how deep the collections nest; in WKB and its hex form, how many there are in all. Past the limit, by default `MAX_GEOM_COLLECTIONS`, 198, the reader raises `ValueError` (`django/contrib/gis/geos/prototypes/io.py:_WKTReader.limit`, `django/contrib/gis/geos/prototypes/io.py:_WKBReader`; [Geometries in Python: GEOS and GDAL](../contrib/geometries.md#geosgeometry)). The constant's comment places 198 just under PostGIS's own ceiling, so that Django refuses first. GeoJSON is parsed by GDAL, and GDAL's WKB of it is counted at the default limit, whatever max_geom_collections says (`django/contrib/gis/gdal/geometries.py:OGRGeometry._geos_ptr`); geometries read back from the database are read with no limit ([Spatial fields: the geometry fields and `RasterField`](../contrib/spatial-fields.md)).

> **Trap.** `GEOSGeometry` and the model field take max_geom_collections, and there `None` means no limit on WKT and WKB. The form field takes it too, and there `None` keeps the default, so a model field given `None` still makes a form field that refuses past 198 (`django/contrib/gis/forms/fields.py:GeometryField.__init__`).
