1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
From efea1ef7e2190e3f77ca0651b5458297bc0f6a9f Mon Sep 17 00:00:00 2001
From: Marc Deslauriers <marc.deslauriers@ubuntu.com>
Date: Wed, 30 Apr 2025 10:34:27 -0400
Subject: [PATCH] Fixed CVE-2024-41991 -- Prevented potential ReDoS in
django.utils.html.urlize() and AdminURLFieldWidget.
Thanks Seokchan Yoon for the report.
Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
CVE: CVE-2024-41991
Upstream-Status: Backport
https://github.com/django/django/commit/efea1ef7e2190e3f77ca0651b5458297bc0f6a9f/
Signed-off-by: Saravanan <saravanan.kadambathursubramaniyam@windriver.com>
---
django/contrib/admin/widgets.py | 2 +-
django/utils/html.py | 10 ++++++++--
docs/releases/3.2.25.txt | 7 +++++++
tests/admin_widgets/tests.py | 7 ++++++-
tests/utils_tests/test_html.py | 13 +++++++++++++
5 files changed, 35 insertions(+), 4 deletions(-)
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index aeb7477..b7dd0d8 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -339,7 +339,7 @@ class AdminURLFieldWidget(forms.URLInput):
context = super().get_context(name, value, attrs)
context['current_label'] = _('Currently:')
context['change_label'] = _('Change:')
- context['widget']['href'] = smart_urlquote(context['widget']['value']) if value else ''
+ context['widget']['href'] = smart_urlquote(context['widget']['value']) if url_valid else ''
context['url_valid'] = url_valid
return context
diff --git a/django/utils/html.py b/django/utils/html.py
index 3bc02b8..44c6b7b 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -29,6 +29,8 @@ simple_url_2_re = _lazy_re_compile(
re.IGNORECASE
)
+MAX_URL_LENGTH = 2048
+
@keep_lazy(str, SafeString)
def escape(text):
@@ -298,6 +300,10 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
except ValueError:
# value contains more than one @.
return False
+ # Max length for domain name labels is 63 characters per RFC 1034.
+ # Helps to avoid ReDoS vectors in the domain part.
+ if len(p2) > 63:
+ return False
# Dot must be in p2 (e.g. example.com)
if '.' not in p2 or p2.startswith('.'):
return False
@@ -316,9 +322,9 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
# Make URL we want to point to.
url = None
nofollow_attr = ' rel="nofollow"' if nofollow else ''
- if simple_url_re.match(middle):
+ if len(middle) <= MAX_URL_LENGTH and simple_url_re.match(middle):
url = smart_urlquote(html.unescape(middle))
- elif simple_url_2_re.match(middle):
+ elif len(middle) <= MAX_URL_LENGTH and simple_url_2_re.match(middle):
url = smart_urlquote('http://%s' % html.unescape(middle))
elif ':' not in middle and is_email_simple(middle):
local, domain = middle.rsplit('@', 1)
diff --git a/docs/releases/3.2.25.txt b/docs/releases/3.2.25.txt
index 60236d5..67dc8a2 100644
--- a/docs/releases/3.2.25.txt
+++ b/docs/releases/3.2.25.txt
@@ -59,6 +59,13 @@ directory-traversal via certain inputs when calling :meth:`save()
Built-in ``Storage`` sub-classes were not affected by this vulnerability.
+CVE-2024-41991: Potential denial-of-service vulnerability in ``django.utils.html.urlize()`` and ``AdminURLFieldWidget``
+===========================================================================================================
+
+:tfilter:`urlize`, :tfilter:`urlizetrunc`, and ``AdminURLFieldWidget`` were
+subject to a potential denial-of-service attack via certain inputs with a very
+large number of Unicode characters.
+
Bugfixes
========
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index f701f1a..c532199 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -353,7 +353,12 @@ class AdminSplitDateTimeWidgetTest(SimpleTestCase):
class AdminURLWidgetTest(SimpleTestCase):
def test_get_context_validates_url(self):
w = widgets.AdminURLFieldWidget()
- for invalid in ['', '/not/a/full/url/', 'javascript:alert("Danger XSS!")']:
+ for invalid in [
+ "",
+ "/not/a/full/url/",
+ 'javascript:alert("Danger XSS!")',
+ "http://" + "한.글." * 1_000_000 + "com",
+ ]:
with self.subTest(url=invalid):
self.assertFalse(w.get_context('name', invalid, {})['url_valid'])
self.assertTrue(w.get_context('name', 'http://example.com', {})['url_valid'])
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index 30f5ba6..93458ac 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -255,6 +255,15 @@ class TestUtilsHtml(SimpleTestCase):
'Search for <a href="http://google.com/?q=">google.com/?q=</a>!'
),
('foo@example.com', '<a href="mailto:foo@example.com">foo@example.com</a>'),
+ (
+ "test@" + "한.글." * 15 + "aaa",
+ '<a href="mailto:test@'
+ + "xn--6q8b.xn--bj0b." * 15
+ + 'aaa">'
+ + "test@"
+ + "한.글." * 15
+ + "aaa</a>",
+ ),
)
for value, output in tests:
with self.subTest(value=value):
@@ -263,6 +272,10 @@ class TestUtilsHtml(SimpleTestCase):
def test_urlize_unchanged_inputs(self):
tests = (
('a' + '@a' * 50000) + 'a', # simple_email_re catastrophic test
+ # Unicode domain catastrophic tests.
+ "a@" + "한.글." * 1_000_000 + "a",
+ "http://" + "한.글." * 1_000_000 + "com",
+ "www." + "한.글." * 1_000_000 + "com",
('a' + '.' * 1000000) + 'a', # trailing_punctuation catastrophic test
'foo@',
'@foo.com',
--
2.40.0
|