summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-03-06 15:24:56 +0100
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-04-02 10:42:15 +0200
commit8c6871b097b6c49d2a782c0d80d908bcbe2116f1 (patch)
tree521c270381ec399e0da4c1c7eaf31d0484bfe1f6
parent2be56bc534a1ef7c9bae63182e6053513daa0d25 (diff)
[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.
-rw-r--r--django/core/validators.py3
-rw-r--r--django/utils/html.py3
-rw-r--r--django/utils/http.py6
-rw-r--r--docs/releases/5.0.14.txt10
-rw-r--r--tests/utils_tests/test_http.py16
5 files changed, 34 insertions, 4 deletions
diff --git a/django/core/validators.py b/django/core/validators.py
index fe8d46526a..14b89ff11e 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -7,6 +7,7 @@ from urllib.parse import urlsplit, urlunsplit
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
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.regex_helper import _lazy_re_compile
from django.utils.translation import gettext_lazy as _
@@ -104,7 +105,7 @@ class URLValidator(RegexValidator):
message = _("Enter a valid URL.")
schemes = ["http", "https", "ftp", "ftps"]
unsafe_chars = frozenset("\t\r\n")
- max_length = 2048
+ max_length = MAX_URL_LENGTH
def __init__(self, schemes=None, **kwargs):
super().__init__(**kwargs)
diff --git a/django/utils/html.py b/django/utils/html.py
index d04e594d13..df0b3984df 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -11,7 +11,7 @@ from django.core.exceptions import SuspiciousOperation
from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.encoding import punycode
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.safestring import SafeData, SafeString, mark_safe
from django.utils.text import normalize_newlines
@@ -37,7 +37,6 @@ VOID_ELEMENTS = {
"spacer",
}
-MAX_URL_LENGTH = 2048
MAX_STRIP_TAGS_DEPTH = 50
diff --git a/django/utils/http.py b/django/utils/http.py
index cfd982fc01..cc340ce849 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -37,6 +37,7 @@ ASCTIME_DATE = _lazy_re_compile(r"^\w{3} %s %s %s %s$" % (__M, __D2, __T, __Y))
RFC3986_GENDELIMS = ":/?#[]@"
RFC3986_SUBDELIMS = "!$&'()*+,;="
+MAX_URL_LENGTH = 2048
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):
# 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.
- 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
try:
url_info = urlparse(url)
diff --git a/docs/releases/5.0.14.txt b/docs/releases/5.0.14.txt
index 8684a270dc..230bfed652 100644
--- a/docs/releases/5.0.14.txt
+++ b/docs/releases/5.0.14.txt
@@ -5,3 +5,13 @@ Django 5.0.14 release notes
*April 2, 2025*
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.
diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
index 818c35e597..20fe2c4c70 100644
--- a/tests/utils_tests/test_http.py
+++ b/tests/utils_tests/test_http.py
@@ -6,6 +6,7 @@ from unittest import mock
from django.test import SimpleTestCase
from django.utils.datastructures import MultiValueDict
from django.utils.http import (
+ MAX_URL_LENGTH,
base36_to_int,
content_disposition_header,
escape_leading_slashes,
@@ -273,6 +274,21 @@ class URLHasAllowedHostAndSchemeTests(unittest.TestCase):
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):
def test_roundtrip(self):