Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions django/http/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,13 +632,20 @@ def set_headers(self, filelike):
class HttpResponseRedirectBase(HttpResponse):
allowed_schemes = ["http", "https", "ftp"]

def __init__(self, redirect_to, preserve_request=False, *args, **kwargs):
def __init__(
self,
redirect_to,
preserve_request=False,
*args,
max_length=MAX_URL_REDIRECT_LENGTH,
**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_REDIRECT_LENGTH:
if max_length is not None and len(redirect_to_str) > max_length:
raise DisallowedRedirect(
f"Unsafe redirect exceeding {MAX_URL_REDIRECT_LENGTH} characters"
f"Unsafe redirect exceeding {max_length} characters"
)
parsed = urlsplit(redirect_to_str)
if preserve_request:
Expand Down
11 changes: 10 additions & 1 deletion django/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.template import loader
from django.urls import NoReverseMatch, reverse
from django.utils.functional import Promise
from django.utils.http import MAX_URL_REDIRECT_LENGTH
from django.utils.translation import gettext as _


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


def redirect(to, *args, permanent=False, preserve_request=False, **kwargs):
def redirect(
to,
*args,
permanent=False,
preserve_request=False,
max_length=MAX_URL_REDIRECT_LENGTH,
**kwargs,
):
"""
Return an HttpResponseRedirect to the appropriate URL for the arguments
passed.
Expand All @@ -51,6 +59,7 @@ def redirect(to, *args, permanent=False, preserve_request=False, **kwargs):
return redirect_class(
resolve_url(to, *args, **kwargs),
preserve_request=preserve_request,
max_length=max_length,
)


Expand Down
5 changes: 2 additions & 3 deletions docs/ref/models/querysets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2643,9 +2643,8 @@ iterator` if you call its asynchronous version ``aiterator``.

A ``QuerySet`` typically caches its results internally so that repeated
evaluations do not result in additional queries. In contrast, ``iterator()``
will read results directly, without doing any caching at the ``QuerySet`` level
(internally, the default iterator calls ``iterator()`` and caches the return
value). For a ``QuerySet`` which returns a large number of objects that you
will read results directly, without doing any caching at the ``QuerySet``
level. For a ``QuerySet`` which returns a large number of objects that you
only need to access once, this can result in better performance and a
significant reduction in memory.

Expand Down
8 changes: 8 additions & 0 deletions docs/ref/request-response.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1142,8 +1142,16 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in
that defaults to ``False``, producing a response with a 302 status code. If
``preserve_request`` is ``True``, the status code will be 307 instead.

The constructor accepts an optional ``max_length`` keyword argument to
override the maximum allowed length for the redirect URL. You can set it
to ``None`` to disable the length check.

See :class:`HttpResponse` for other optional constructor arguments.

.. versionchanged:: 6.1

``max_length`` was added.

.. attribute:: HttpResponseRedirect.url

This read-only attribute represents the URL the response will redirect
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ Requests and Responses
* :attr:`HttpRequest.multipart_parser_class <django.http.HttpRequest.multipart_parser_class>`
can now be customized to use a different multipart parser class.

* :class:`~django.http.HttpResponseRedirect` (and its subclasses), as well as
the :func:`~django.shortcuts.redirect` shortcut, now accept a ``max_length``
parameter to override the default maximum URL length limit.

Security
~~~~~~~~

Expand Down
10 changes: 9 additions & 1 deletion docs/topics/http/shortcuts.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ This example is equivalent to::
``redirect()``
==============

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

Returns an :class:`~django.http.HttpResponseRedirect` to the appropriate URL
for the arguments passed.
Expand Down Expand Up @@ -125,6 +125,14 @@ This example is equivalent to::
``True`` ``True`` 308
========= ================ ================

An optional ``max_length`` keyword argument can be provided to override
the maximum allowed length for the redirect URL. Set it to ``None`` to
disable the length check.

.. versionchanged:: 6.1

``max_length`` was added.

Examples
--------

Expand Down
44 changes: 33 additions & 11 deletions tests/httpwrappers/tests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import copy
import itertools
import json
import os
import pickle
Expand Down Expand Up @@ -488,15 +489,24 @@ def test_stream_interface(self):

def test_redirect_url_max_length(self):
base_url = "https://example.com/"
for length in (
MAX_URL_REDIRECT_LENGTH - 1,
MAX_URL_REDIRECT_LENGTH,
for length, response_class in itertools.product(
(MAX_URL_REDIRECT_LENGTH - 1, MAX_URL_REDIRECT_LENGTH),
(HttpResponseRedirect, HttpResponsePermanentRedirect),
):
long_url = base_url + "x" * (length - len(base_url))
with self.subTest(length=length):
response = HttpResponseRedirect(long_url)
with self.subTest(length=length, response_class=response_class):
response = response_class(long_url)
self.assertEqual(response.url, long_url)
response = HttpResponsePermanentRedirect(long_url)

def test_redirect_url_max_length_override_via_param(self):
base_url = "https://example.com/"
for (max_length, length), response_class in itertools.product(
((None, MAX_URL_REDIRECT_LENGTH + 1), (100, 99), (100, 100)),
(HttpResponseRedirect, HttpResponsePermanentRedirect),
):
long_url = base_url + "x" * (length - len(base_url))
with self.subTest(length=length, response_class=response_class):
response = response_class(long_url, max_length=max_length)
self.assertEqual(response.url, long_url)

def test_unsafe_redirect(self):
Expand All @@ -506,11 +516,23 @@ def test_unsafe_redirect(self):
"file:///etc/passwd",
"é" * (MAX_URL_REDIRECT_LENGTH + 1),
]
for url in bad_urls:
with self.assertRaises(DisallowedRedirect):
HttpResponseRedirect(url)
with self.assertRaises(DisallowedRedirect):
HttpResponsePermanentRedirect(url)
for url, response_class in itertools.product(
bad_urls, (HttpResponseRedirect, HttpResponsePermanentRedirect)
):
with (
self.subTest(url=url, response_class=response_class),
self.assertRaises(DisallowedRedirect),
):
response_class(url)

def test_unsafe_redirect_via_max_length(self):
url = "https://example.com/"
for response_class in (HttpResponseRedirect, HttpResponsePermanentRedirect):
with (
self.subTest(response_class=response_class),
self.assertRaises(DisallowedRedirect),
):
response_class(url, max_length=len(url) - 1)

def test_header_deletion(self):
r = HttpResponse("hello")
Expand Down
18 changes: 18 additions & 0 deletions tests/shortcuts/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django.core.exceptions import DisallowedRedirect
from django.http.response import HttpResponseRedirectBase
from django.shortcuts import redirect
from django.test import SimpleTestCase, override_settings
from django.test.utils import require_jinja2
from django.utils.http import MAX_URL_REDIRECT_LENGTH


@override_settings(ROOT_URLCONF="shortcuts.urls")
Expand Down Expand Up @@ -56,3 +58,19 @@ def test_redirect_response_status_code(self):
)
self.assertIsInstance(response, HttpResponseRedirectBase)
self.assertEqual(response.status_code, expected_status_code)

def test_redirect_max_length_default_raises(self):
long_url = "https://example.com/" + "x" * MAX_URL_REDIRECT_LENGTH
msg = f"Unsafe redirect exceeding {MAX_URL_REDIRECT_LENGTH} characters"
with self.assertRaisesMessage(DisallowedRedirect, msg):
redirect(long_url)

def test_redirect_max_length_override_passes(self):
long_url = "https://example.com/" + "x" * MAX_URL_REDIRECT_LENGTH
response = redirect(long_url, max_length=None)
self.assertEqual(response.url, long_url)

def test_redirect_custom_strict_limit_raises(self):
msg = "Unsafe redirect exceeding 5 characters"
with self.assertRaisesMessage(DisallowedRedirect, msg):
redirect("https://example.com/", max_length=5)
Loading