Skip to content

Commit 523da87

Browse files
felixxmsarahboyce
andcommitted
[5.0.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 7b7b909 commit 523da87

File tree

6 files changed

+42
-4
lines changed

6 files changed

+42
-4
lines changed

django/contrib/admin/widgets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ def get_context(self, name, value, attrs):
390390
context["current_label"] = _("Currently:")
391391
context["change_label"] = _("Change:")
392392
context["widget"]["href"] = (
393-
smart_urlquote(context["widget"]["value"]) if value else ""
393+
smart_urlquote(context["widget"]["value"]) if url_valid else ""
394394
)
395395
context["url_valid"] = url_valid
396396
return context

django/utils/html.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
"spacer",
3737
}
3838

39+
MAX_URL_LENGTH = 2048
40+
3941

4042
@keep_lazy(SafeString)
4143
def escape(text):
@@ -330,9 +332,9 @@ def handle_word(
330332
# Make URL we want to point to.
331333
url = None
332334
nofollow_attr = ' rel="nofollow"' if nofollow else ""
333-
if self.simple_url_re.match(middle):
335+
if len(middle) <= MAX_URL_LENGTH and self.simple_url_re.match(middle):
334336
url = smart_urlquote(html.unescape(middle))
335-
elif self.simple_url_2_re.match(middle):
337+
elif len(middle) <= MAX_URL_LENGTH and self.simple_url_2_re.match(middle):
336338
url = smart_urlquote("http://%s" % html.unescape(middle))
337339
elif ":" not in middle and self.is_email_simple(middle):
338340
local, domain = middle.rsplit("@", 1)
@@ -447,6 +449,10 @@ def is_email_simple(value):
447449
except ValueError:
448450
# value contains more than one @.
449451
return False
452+
# Max length for domain name labels is 63 characters per RFC 1034.
453+
# Helps to avoid ReDoS vectors in the domain part.
454+
if len(p2) > 63:
455+
return False
450456
# Dot must be in p2 (e.g. example.com)
451457
if "." not in p2 or p2.startswith("."):
452458
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

docs/releases/5.0.8.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
@@ -462,7 +462,12 @@ def test_localization(self):
462462
class AdminURLWidgetTest(SimpleTestCase):
463463
def test_get_context_validates_url(self):
464464
w = widgets.AdminURLFieldWidget()
465-
for invalid in ["", "/not/a/full/url/", 'javascript:alert("Danger XSS!")']:
465+
for invalid in [
466+
"",
467+
"/not/a/full/url/",
468+
'javascript:alert("Danger XSS!")',
469+
"http://" + "한.글." * 1_000_000 + "com",
470+
]:
466471
with self.subTest(url=invalid):
467472
self.assertFalse(w.get_context("name", invalid, {})["url_valid"])
468473
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
@@ -338,6 +338,15 @@ def test_urlize(self):
338338
'Search for <a href="http://google.com/?q=">google.com/?q=</a>!',
339339
),
340340
341+
(
342+
"test@" + "한.글." * 15 + "aaa",
343+
'<a href="mailto:test@'
344+
+ "xn--6q8b.xn--bj0b." * 15
345+
+ 'aaa">'
346+
+ "test@"
347+
+ "한.글." * 15
348+
+ "aaa</a>",
349+
),
341350
)
342351
for value, output in tests:
343352
with self.subTest(value=value):
@@ -346,6 +355,10 @@ def test_urlize(self):
346355
def test_urlize_unchanged_inputs(self):
347356
tests = (
348357
("a" + "@a" * 50000) + "a", # simple_email_re catastrophic test
358+
# Unicode domain catastrophic tests.
359+
"a@" + "한.글." * 1_000_000 + "a",
360+
"http://" + "한.글." * 1_000_000 + "com",
361+
"www." + "한.글." * 1_000_000 + "com",
349362
("a" + "." * 1000000) + "a", # trailing_punctuation catastrophic test
350363
"foo@",
351364
"@foo.com",

0 commit comments

Comments
 (0)