Skip to content

Commit efea1ef

Browse files
felixxmsarahboyce
andcommitted
[4.2.x] 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 <[email protected]>
1 parent d0a82e2 commit efea1ef

File tree

5 files changed

+35
-4
lines changed

5 files changed

+35
-4
lines changed

django/contrib/admin/widgets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ def get_context(self, name, value, attrs):
383383
context["current_label"] = _("Currently:")
384384
context["change_label"] = _("Change:")
385385
context["widget"]["href"] = (
386-
smart_urlquote(context["widget"]["value"]) if value else ""
386+
smart_urlquote(context["widget"]["value"]) if url_valid else ""
387387
)
388388
context["url_valid"] = url_valid
389389
return context

django/utils/html.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from django.utils.safestring import SafeData, SafeString, mark_safe
1414
from django.utils.text import normalize_newlines
1515

16+
MAX_URL_LENGTH = 2048
17+
1618

1719
@keep_lazy(SafeString)
1820
def escape(text):
@@ -300,9 +302,9 @@ def handle_word(
300302
# Make URL we want to point to.
301303
url = None
302304
nofollow_attr = ' rel="nofollow"' if nofollow else ""
303-
if self.simple_url_re.match(middle):
305+
if len(middle) <= MAX_URL_LENGTH and self.simple_url_re.match(middle):
304306
url = smart_urlquote(html.unescape(middle))
305-
elif self.simple_url_2_re.match(middle):
307+
elif len(middle) <= MAX_URL_LENGTH and self.simple_url_2_re.match(middle):
306308
url = smart_urlquote("http://%s" % html.unescape(middle))
307309
elif ":" not in middle and self.is_email_simple(middle):
308310
local, domain = middle.rsplit("@", 1)
@@ -417,6 +419,10 @@ def is_email_simple(value):
417419
except ValueError:
418420
# value contains more than one @.
419421
return False
422+
# Max length for domain name labels is 63 characters per RFC 1034.
423+
# Helps to avoid ReDoS vectors in the domain part.
424+
if len(p2) > 63:
425+
return False
420426
# Dot must be in p2 (e.g. example.com)
421427
if "." not in p2 or p2.startswith("."):
422428
return False

docs/releases/4.2.15.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ CVE-2024-41990: Potential denial-of-service vulnerability in ``django.utils.html
2323
denial-of-service attack via very large inputs with a specific sequence of
2424
characters.
2525

26+
CVE-2024-41991: Potential denial-of-service vulnerability in ``django.utils.html.urlize()`` and ``AdminURLFieldWidget``
27+
=======================================================================================================================
28+
29+
:tfilter:`urlize`, :tfilter:`urlizetrunc`, and ``AdminURLFieldWidget`` were
30+
subject to a potential denial-of-service attack via certain inputs with a very
31+
large number of Unicode characters.
32+
2633
Bugfixes
2734
========
2835

tests/admin_widgets/tests.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,12 @@ def test_localization(self):
461461
class AdminURLWidgetTest(SimpleTestCase):
462462
def test_get_context_validates_url(self):
463463
w = widgets.AdminURLFieldWidget()
464-
for invalid in ["", "/not/a/full/url/", 'javascript:alert("Danger XSS!")']:
464+
for invalid in [
465+
"",
466+
"/not/a/full/url/",
467+
'javascript:alert("Danger XSS!")',
468+
"http://" + "한.글." * 1_000_000 + "com",
469+
]:
465470
with self.subTest(url=invalid):
466471
self.assertFalse(w.get_context("name", invalid, {})["url_valid"])
467472
self.assertTrue(w.get_context("name", "http://example.com", {})["url_valid"])

tests/utils_tests/test_html.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,15 @@ def test_urlize(self):
328328
'Search for <a href="http://google.com/?q=">google.com/?q=</a>!',
329329
),
330330
331+
(
332+
"test@" + "한.글." * 15 + "aaa",
333+
'<a href="mailto:test@'
334+
+ "xn--6q8b.xn--bj0b." * 15
335+
+ 'aaa">'
336+
+ "test@"
337+
+ "한.글." * 15
338+
+ "aaa</a>",
339+
),
331340
)
332341
for value, output in tests:
333342
with self.subTest(value=value):
@@ -336,6 +345,10 @@ def test_urlize(self):
336345
def test_urlize_unchanged_inputs(self):
337346
tests = (
338347
("a" + "@a" * 50000) + "a", # simple_email_re catastrophic test
348+
# Unicode domain catastrophic tests.
349+
"a@" + "한.글." * 1_000_000 + "a",
350+
"http://" + "한.글." * 1_000_000 + "com",
351+
"www." + "한.글." * 1_000_000 + "com",
339352
("a" + "." * 1000000) + "a", # trailing_punctuation catastrophic test
340353
"foo@",
341354
"@foo.com",

0 commit comments

Comments
 (0)