1
0
mirror of https://github.com/django/django.git synced 2025-06-02 10:09:12 +00:00

[5.0.x] Fixed CVE-2025-27556 -- Mitigated potential DoS in url_has_allowed_host_and_scheme() on Windows.

Thank you sw0rd1ight for the report.

Backport of 39e2297210d9d2938c75fc911d45f0e863dc4821 from main.
This commit is contained in:
Sarah Boyce 2025-03-06 15:24:56 +01:00
parent 2be56bc534
commit 8c6871b097
5 changed files with 34 additions and 4 deletions

View File

@ -7,6 +7,7 @@ from urllib.parse import urlsplit, urlunsplit
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
from django.utils.encoding import punycode from django.utils.encoding import punycode
from django.utils.http import MAX_URL_LENGTH
from django.utils.ipv6 import is_valid_ipv6_address from django.utils.ipv6 import is_valid_ipv6_address
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -104,7 +105,7 @@ class URLValidator(RegexValidator):
message = _("Enter a valid URL.") message = _("Enter a valid URL.")
schemes = ["http", "https", "ftp", "ftps"] schemes = ["http", "https", "ftp", "ftps"]
unsafe_chars = frozenset("\t\r\n") unsafe_chars = frozenset("\t\r\n")
max_length = 2048 max_length = MAX_URL_LENGTH
def __init__(self, schemes=None, **kwargs): def __init__(self, schemes=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)

View File

@ -11,7 +11,7 @@ from django.core.exceptions import SuspiciousOperation
from django.utils.deprecation import RemovedInDjango60Warning from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.encoding import punycode from django.utils.encoding import punycode
from django.utils.functional import Promise, cached_property, keep_lazy, keep_lazy_text from django.utils.functional import Promise, cached_property, keep_lazy, keep_lazy_text
from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS from django.utils.http import MAX_URL_LENGTH, RFC3986_GENDELIMS, RFC3986_SUBDELIMS
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
from django.utils.safestring import SafeData, SafeString, mark_safe from django.utils.safestring import SafeData, SafeString, mark_safe
from django.utils.text import normalize_newlines from django.utils.text import normalize_newlines
@ -37,7 +37,6 @@ VOID_ELEMENTS = {
"spacer", "spacer",
} }
MAX_URL_LENGTH = 2048
MAX_STRIP_TAGS_DEPTH = 50 MAX_STRIP_TAGS_DEPTH = 50

View File

@ -37,6 +37,7 @@ ASCTIME_DATE = _lazy_re_compile(r"^\w{3} %s %s %s %s$" % (__M, __D2, __T, __Y))
RFC3986_GENDELIMS = ":/?#[]@" RFC3986_GENDELIMS = ":/?#[]@"
RFC3986_SUBDELIMS = "!$&'()*+,;=" RFC3986_SUBDELIMS = "!$&'()*+,;="
MAX_URL_LENGTH = 2048
def urlencode(query, doseq=False): def urlencode(query, doseq=False):
@ -273,7 +274,10 @@ def url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False): def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
# Chrome considers any URL with more than two slashes to be absolute, but # Chrome considers any URL with more than two slashes to be absolute, but
# urlparse is not so flexible. Treat any url with three slashes as unsafe. # urlparse is not so flexible. Treat any url with three slashes as unsafe.
if url.startswith("///"): if url.startswith("///") or len(url) > MAX_URL_LENGTH:
# urlparse does not perform validation of inputs. Unicode normalization
# is very slow on Windows and can be a DoS attack vector.
# https://docs.python.org/3/library/urllib.parse.html#url-parsing-security
return False return False
try: try:
url_info = urlparse(url) url_info = urlparse(url)

View File

@ -5,3 +5,13 @@ Django 5.0.14 release notes
*April 2, 2025* *April 2, 2025*
Django 5.0.14 fixes a security issue with severity "moderate" in 5.0.13. Django 5.0.14 fixes a security issue with severity "moderate" in 5.0.13.
CVE-2025-27556: Potential denial-of-service vulnerability in ``LoginView``, ``LogoutView``, and ``set_language()`` on Windows
=============================================================================================================================
Python's :func:`NFKC normalization <python:unicodedata.normalize>` is slow on
Windows. As a consequence, :class:`~django.contrib.auth.views.LoginView`,
:class:`~django.contrib.auth.views.LogoutView`, and
:func:`~django.views.i18n.set_language` were subject to a potential
denial-of-service attack via certain inputs with a very large number of Unicode
characters.

View File

@ -6,6 +6,7 @@ from unittest import mock
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.http import ( from django.utils.http import (
MAX_URL_LENGTH,
base36_to_int, base36_to_int,
content_disposition_header, content_disposition_header,
escape_leading_slashes, escape_leading_slashes,
@ -273,6 +274,21 @@ class URLHasAllowedHostAndSchemeTests(unittest.TestCase):
False, False,
) )
def test_max_url_length(self):
allowed_host = "example.com"
max_extra_characters = "é" * (MAX_URL_LENGTH - len(allowed_host) - 1)
max_length_boundary_url = f"{allowed_host}/{max_extra_characters}"
cases = [
(max_length_boundary_url, True),
(max_length_boundary_url + "ú", False),
]
for url, expected in cases:
with self.subTest(url=url):
self.assertIs(
url_has_allowed_host_and_scheme(url, allowed_hosts={allowed_host}),
expected,
)
class URLSafeBase64Tests(unittest.TestCase): class URLSafeBase64Tests(unittest.TestCase):
def test_roundtrip(self): def test_roundtrip(self):