Skip to content

Commit d75d57c

Browse files
varunkasyapjacobtylerwalls
authored andcommitted
Fixed #36767 -- Allowed max redirect URL length to be set on HttpResponseRedirect.
1 parent 7e7e969 commit d75d57c

7 files changed

Lines changed: 92 additions & 16 deletions

File tree

django/http/response.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -632,13 +632,20 @@ def set_headers(self, filelike):
632632
class HttpResponseRedirectBase(HttpResponse):
633633
allowed_schemes = ["http", "https", "ftp"]
634634

635-
def __init__(self, redirect_to, preserve_request=False, *args, **kwargs):
635+
def __init__(
636+
self,
637+
redirect_to,
638+
preserve_request=False,
639+
*args,
640+
max_length=MAX_URL_REDIRECT_LENGTH,
641+
**kwargs,
642+
):
636643
super().__init__(*args, **kwargs)
637644
self["Location"] = iri_to_uri(redirect_to)
638645
redirect_to_str = str(redirect_to)
639-
if len(redirect_to_str) > MAX_URL_REDIRECT_LENGTH:
646+
if max_length is not None and len(redirect_to_str) > max_length:
640647
raise DisallowedRedirect(
641-
f"Unsafe redirect exceeding {MAX_URL_REDIRECT_LENGTH} characters"
648+
f"Unsafe redirect exceeding {max_length} characters"
642649
)
643650
parsed = urlsplit(redirect_to_str)
644651
if preserve_request:

django/shortcuts.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django.template import loader
1414
from django.urls import NoReverseMatch, reverse
1515
from django.utils.functional import Promise
16+
from django.utils.http import MAX_URL_REDIRECT_LENGTH
1617
from django.utils.translation import gettext as _
1718

1819

@@ -27,7 +28,14 @@ def render(
2728
return HttpResponse(content, content_type, status)
2829

2930

30-
def redirect(to, *args, permanent=False, preserve_request=False, **kwargs):
31+
def redirect(
32+
to,
33+
*args,
34+
permanent=False,
35+
preserve_request=False,
36+
max_length=MAX_URL_REDIRECT_LENGTH,
37+
**kwargs,
38+
):
3139
"""
3240
Return an HttpResponseRedirect to the appropriate URL for the arguments
3341
passed.
@@ -51,6 +59,7 @@ def redirect(to, *args, permanent=False, preserve_request=False, **kwargs):
5159
return redirect_class(
5260
resolve_url(to, *args, **kwargs),
5361
preserve_request=preserve_request,
62+
max_length=max_length,
5463
)
5564

5665

docs/ref/request-response.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,8 +1142,16 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in
11421142
that defaults to ``False``, producing a response with a 302 status code. If
11431143
``preserve_request`` is ``True``, the status code will be 307 instead.
11441144

1145+
The constructor accepts an optional ``max_length`` keyword argument to
1146+
override the maximum allowed length for the redirect URL. You can set it
1147+
to ``None`` to disable the length check.
1148+
11451149
See :class:`HttpResponse` for other optional constructor arguments.
11461150

1151+
.. versionchanged:: 6.1
1152+
1153+
``max_length`` was added.
1154+
11471155
.. attribute:: HttpResponseRedirect.url
11481156

11491157
This read-only attribute represents the URL the response will redirect

docs/releases/6.1.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ Requests and Responses
349349
* :attr:`HttpRequest.multipart_parser_class <django.http.HttpRequest.multipart_parser_class>`
350350
can now be customized to use a different multipart parser class.
351351

352+
* :class:`~django.http.HttpResponseRedirect` (and its subclasses), as well as
353+
the :func:`~django.shortcuts.redirect` shortcut, now accept a ``max_length``
354+
parameter to override the default maximum URL length limit.
355+
352356
Security
353357
~~~~~~~~
354358

docs/topics/http/shortcuts.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ This example is equivalent to::
9191
``redirect()``
9292
==============
9393

94-
.. function:: redirect(to, *args, permanent=False, preserve_request=False, **kwargs)
94+
.. function:: redirect(to, *args, permanent=False, preserve_request=False, max_length=MAX_URL_REDIRECT_LENGTH, **kwargs)
9595

9696
Returns an :class:`~django.http.HttpResponseRedirect` to the appropriate URL
9797
for the arguments passed.
@@ -125,6 +125,14 @@ This example is equivalent to::
125125
``True`` ``True`` 308
126126
========= ================ ================
127127

128+
An optional ``max_length`` keyword argument can be provided to override
129+
the maximum allowed length for the redirect URL. Set it to ``None`` to
130+
disable the length check.
131+
132+
.. versionchanged:: 6.1
133+
134+
``max_length`` was added.
135+
128136
Examples
129137
--------
130138

tests/httpwrappers/tests.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import itertools
23
import json
34
import os
45
import pickle
@@ -488,15 +489,24 @@ def test_stream_interface(self):
488489

489490
def test_redirect_url_max_length(self):
490491
base_url = "https://example.com/"
491-
for length in (
492-
MAX_URL_REDIRECT_LENGTH - 1,
493-
MAX_URL_REDIRECT_LENGTH,
492+
for length, response_class in itertools.product(
493+
(MAX_URL_REDIRECT_LENGTH - 1, MAX_URL_REDIRECT_LENGTH),
494+
(HttpResponseRedirect, HttpResponsePermanentRedirect),
494495
):
495496
long_url = base_url + "x" * (length - len(base_url))
496-
with self.subTest(length=length):
497-
response = HttpResponseRedirect(long_url)
497+
with self.subTest(length=length, response_class=response_class):
498+
response = response_class(long_url)
498499
self.assertEqual(response.url, long_url)
499-
response = HttpResponsePermanentRedirect(long_url)
500+
501+
def test_redirect_url_max_length_override_via_param(self):
502+
base_url = "https://example.com/"
503+
for (max_length, length), response_class in itertools.product(
504+
((None, MAX_URL_REDIRECT_LENGTH + 1), (100, 99), (100, 100)),
505+
(HttpResponseRedirect, HttpResponsePermanentRedirect),
506+
):
507+
long_url = base_url + "x" * (length - len(base_url))
508+
with self.subTest(length=length, response_class=response_class):
509+
response = response_class(long_url, max_length=max_length)
500510
self.assertEqual(response.url, long_url)
501511

502512
def test_unsafe_redirect(self):
@@ -506,11 +516,23 @@ def test_unsafe_redirect(self):
506516
"file:///etc/passwd",
507517
"é" * (MAX_URL_REDIRECT_LENGTH + 1),
508518
]
509-
for url in bad_urls:
510-
with self.assertRaises(DisallowedRedirect):
511-
HttpResponseRedirect(url)
512-
with self.assertRaises(DisallowedRedirect):
513-
HttpResponsePermanentRedirect(url)
519+
for url, response_class in itertools.product(
520+
bad_urls, (HttpResponseRedirect, HttpResponsePermanentRedirect)
521+
):
522+
with (
523+
self.subTest(url=url, response_class=response_class),
524+
self.assertRaises(DisallowedRedirect),
525+
):
526+
response_class(url)
527+
528+
def test_unsafe_redirect_via_max_length(self):
529+
url = "https://example.com/"
530+
for response_class in (HttpResponseRedirect, HttpResponsePermanentRedirect):
531+
with (
532+
self.subTest(response_class=response_class),
533+
self.assertRaises(DisallowedRedirect),
534+
):
535+
response_class(url, max_length=len(url) - 1)
514536

515537
def test_header_deletion(self):
516538
r = HttpResponse("hello")

tests/shortcuts/tests.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from django.core.exceptions import DisallowedRedirect
12
from django.http.response import HttpResponseRedirectBase
23
from django.shortcuts import redirect
34
from django.test import SimpleTestCase, override_settings
45
from django.test.utils import require_jinja2
6+
from django.utils.http import MAX_URL_REDIRECT_LENGTH
57

68

79
@override_settings(ROOT_URLCONF="shortcuts.urls")
@@ -56,3 +58,19 @@ def test_redirect_response_status_code(self):
5658
)
5759
self.assertIsInstance(response, HttpResponseRedirectBase)
5860
self.assertEqual(response.status_code, expected_status_code)
61+
62+
def test_redirect_max_length_default_raises(self):
63+
long_url = "https://example.com/" + "x" * MAX_URL_REDIRECT_LENGTH
64+
msg = f"Unsafe redirect exceeding {MAX_URL_REDIRECT_LENGTH} characters"
65+
with self.assertRaisesMessage(DisallowedRedirect, msg):
66+
redirect(long_url)
67+
68+
def test_redirect_max_length_override_passes(self):
69+
long_url = "https://example.com/" + "x" * MAX_URL_REDIRECT_LENGTH
70+
response = redirect(long_url, max_length=None)
71+
self.assertEqual(response.url, long_url)
72+
73+
def test_redirect_custom_strict_limit_raises(self):
74+
msg = "Unsafe redirect exceeding 5 characters"
75+
with self.assertRaisesMessage(DisallowedRedirect, msg):
76+
redirect("https://example.com/", max_length=5)

0 commit comments

Comments
 (0)