Input validation / URL length check (redirects)

HIGH
django/django
Commit: a8cf8c292cfe
Affected: Django 5.1.x series prior to this commit (pre-fix).
2026-04-05 13:42 UTC

Description

The commit fixes a security-related input validation issue in URL redirects by increasing the maximum allowed redirect URL length. Previously HttpResponseRedirectBase used MAX_URL_LENGTH (2048) to guard redirects, which was too restrictive and could disrupt legitimate redirects to third-party services. The patch introduces MAX_URL_REDIRECT_LENGTH (16384) and uses it to validate redirect targets, addressing CVE-2025-64458. The change is not merely a dependency bump; it alters runtime validation logic and adds tests to cover redirects up to the new limit.

Proof of Concept

PoC (exploit steps to reproduce before/after the fix): 1) Set up a minimal Django view that redirects to a long URL. views.py from django.http import HttpResponseRedirect def redirect_too_long(request): base = "https://example.com/redirect?u=" # Create a URL whose total length is just over 2048 but well under 16384 long_path = "a" * (2048 - len(base) + 1) url = base + long_path return HttpResponseRedirect(url) def redirect_allowed(request): base = "https://example.com/redirect?u=" url = base + ("a" * 3000) return HttpResponseRedirect(url) urls.py (snippet-wise mapping) from django.urls import path from . import views urlpatterns = [ path('redirect-too-long/', views.redirect_too_long), path('redirect-allowed/', views.redirect_allowed), ] 2) Test before/after the fix (conceptual samples): - Pre-fix (Django <= 5.1.x before this commit): - Access /redirect-too-long/ should trigger a DisallowedRedirect and result in a 400 Bad Request with a message similar to "Unsafe redirect exceeding 2048 characters". - Access /redirect-allowed/ should succeed (HTTP 302) to the long URL (within the old 2048 limit, which this one exceeds, so may fail pre-fix depending on exact length). - Post-fix (Django after this commit): - Access /redirect-too-long/ should now succeed if the total URL length is <= 16384 characters. - Access /redirect-allowed/ should also succeed, and both should follow redirects as expected. 3) Verification commands (example): - Pre-fix scenario (will fail with 400 on too-long redirects): curl -i http://localhost:8000/redirect-too-long/ # Expect: HTTP/1.1 400 Bad Request with body mentioning unsafe redirect exceeding 2048 characters - Post-fix scenario (will succeed for long redirects up to new limit): curl -i http://localhost:8000/redirect-too-long/ # Expect: HTTP/1.1 302 Found with Location header pointing to the long URL Notes: - The key change is the replacement of MAX_URL_LENGTH with MAX_URL_REDIRECT_LENGTH for redirect URL validation, increasing the allowed length from 2048 to 16384 characters. - The PoC demonstrates how a redirect that used to be blocked due to length is now allowed, thereby preventing legitimate redirects from being rejected due to overly aggressive input validation.

Commit Details

Author: varunkasyap

Date: 2025-11-26 17:28 UTC

Message:

Fixed #36743 -- Increased URL max length enforced in HttpResponseRedirectBase. Refs CVE-2025-64458. The previous limit of 2048 characters reused the URLValidator constant and proved too restrictive for legitimate redirects to some third-party services. This change introduces a separate `MAX_URL_REDIRECT_LENGTH` constant (defaulting to 16384) and uses it in HttpResponseRedirectBase. Thanks Jacob Walls for report and review.

Triage Assessment

Vulnerability Type: Input validation (URL length)

Confidence: HIGH

Reasoning:

The commit explicitly references CVE-2025-64458 and changes the URL length validation for redirects. It introduces a new MAX_URL_REDIRECT_LENGTH constant and uses it to cap redirect URLs, addressing a vulnerability related to overly restrictive or exploitable redirect handling. The tests and docs reinforce that this is a security fix around redirect URL length validation.

Verification Assessment

Vulnerability Type: Input validation / URL length check (redirects)

Confidence: HIGH

Affected Versions: Django 5.1.x series prior to this commit (pre-fix).

Code Diff

diff --git a/django/http/response.py b/django/http/response.py index 020b2fcf3ab8..9bf0b14df5ef 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -22,7 +22,11 @@ from django.utils.datastructures import CaseInsensitiveMapping from django.utils.encoding import iri_to_uri from django.utils.functional import cached_property -from django.utils.http import MAX_URL_LENGTH, content_disposition_header, http_date +from django.utils.http import ( + MAX_URL_REDIRECT_LENGTH, + content_disposition_header, + http_date, +) from django.utils.regex_helper import _lazy_re_compile _charset_from_content_type_re = _lazy_re_compile( @@ -632,9 +636,9 @@ def __init__(self, redirect_to, preserve_request=False, *args, **kwargs): super().__init__(*args, **kwargs) self["Location"] = iri_to_uri(redirect_to) redirect_to_str = str(redirect_to) - if len(redirect_to_str) > MAX_URL_LENGTH: + if len(redirect_to_str) > MAX_URL_REDIRECT_LENGTH: raise DisallowedRedirect( - f"Unsafe redirect exceeding {MAX_URL_LENGTH} characters" + f"Unsafe redirect exceeding {MAX_URL_REDIRECT_LENGTH} characters" ) parsed = urlsplit(redirect_to_str) if preserve_request: diff --git a/django/utils/http.py b/django/utils/http.py index 21d5822bf291..2950f3e69500 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -39,6 +39,7 @@ RFC3986_GENDELIMS = ":/?#[]@" RFC3986_SUBDELIMS = "!$&'()*+,;=" MAX_URL_LENGTH = 2048 +MAX_URL_REDIRECT_LENGTH = 16384 def urlencode(query, doseq=False): diff --git a/docs/releases/5.2.9.txt b/docs/releases/5.2.9.txt index edd82271d99e..8a8000a9f112 100644 --- a/docs/releases/5.2.9.txt +++ b/docs/releases/5.2.9.txt @@ -21,3 +21,8 @@ Bugfixes * Fixed a regression in Django 5.2.2 that caused a crash when using aggregate functions with an empty ``Q`` filter over a queryset with annotations (:ticket:`36751`). + +* Fixed a regression in Django 5.2.8 where ``DisallowedRedirect`` was raised by + :class:`~django.http.HttpResponseRedirect` and + :class:`~django.http.HttpResponsePermanentRedirect` for URLs longer than 2048 + characters. The limit is now 16384 characters (:ticket:`36743`). diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 804bec50b08b..3e8364e616de 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -24,7 +24,7 @@ ) from django.test import SimpleTestCase from django.utils.functional import lazystr -from django.utils.http import MAX_URL_LENGTH +from django.utils.http import MAX_URL_REDIRECT_LENGTH class QueryDictTests(SimpleTestCase): @@ -486,12 +486,25 @@ def test_stream_interface(self): r.writelines(["foo\n", "bar\n", "baz\n"]) self.assertEqual(r.content, b"foo\nbar\nbaz\n") + def test_redirect_url_max_length(self): + base_url = "https://example.com/" + for length in ( + MAX_URL_REDIRECT_LENGTH - 1, + MAX_URL_REDIRECT_LENGTH, + ): + long_url = base_url + "x" * (length - len(base_url)) + with self.subTest(length=length): + response = HttpResponseRedirect(long_url) + self.assertEqual(response.url, long_url) + response = HttpResponsePermanentRedirect(long_url) + self.assertEqual(response.url, long_url) + def test_unsafe_redirect(self): bad_urls = [ 'data:text/html,<script>window.alert("xss")</script>', "mailto:test@example.com", "file:///etc/passwd", - "é" * (MAX_URL_LENGTH + 1), + "é" * (MAX_URL_REDIRECT_LENGTH + 1), ] for url in bad_urls: with self.assertRaises(DisallowedRedirect):
← Back to Alerts View on GitHub →