From c19465ad87e33b6122c886b97a202ad54cd43672 Mon Sep 17 00:00:00 2001
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Date: Fri, 12 Jul 2024 11:38:34 +0200
Subject: [PATCH] Fixed CVE-2024-41989 -- Prevented excessive memory
 consumption in floatformat.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Thanks Elias Myllymäki for the report.

Co-authored-by: Shai Berger <shai@platonix.com>
---
 django/template/defaultfilters.py               | 13 +++++++++++++
 docs/releases/4.2.15.txt                        |  9 +++++++++
 docs/releases/5.0.8.txt                         |  9 +++++++++
 .../filter_tests/test_floatformat.py            | 17 +++++++++++++++++
 4 files changed, 48 insertions(+)

diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index 02cac06bcf..66c6e76d20 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -162,6 +162,19 @@ def floatformat(text, arg=-1):
     except ValueError:
         return input_val
 
+    _, digits, exponent = d.as_tuple()
+    try:
+        number_of_digits_and_exponent_sum = len(digits) + abs(exponent)
+    except TypeError:
+        # Exponent values can be "F", "n", "N".
+        number_of_digits_and_exponent_sum = 0
+
+    # Values with more than 200 digits, or with a large exponent, are returned "as is"
+    # to avoid high memory consumption and potential denial-of-service attacks.
+    # The cut-off of 200 is consistent with django.utils.numberformat.floatformat().
+    if number_of_digits_and_exponent_sum > 200:
+        return input_val
+
     try:
         m = int(d) - d
     except (ValueError, OverflowError, InvalidOperation):
diff --git a/docs/releases/4.2.15.txt b/docs/releases/4.2.15.txt
index d312f8580f..f3fdb0a3cf 100644
--- a/docs/releases/4.2.15.txt
+++ b/docs/releases/4.2.15.txt
@@ -7,6 +7,15 @@ Django 4.2.15 release notes
 Django 4.2.15 fixes three security issues with severity "moderate", one
 security issue with severity "high", and a regression in 4.2.14.
 
+CVE-2024-41989: Memory exhaustion in ``django.utils.numberformat.floatformat()``
+================================================================================
+
+If :tfilter:`floatformat` received a string representation of a number in
+scientific notation with a large exponent, it could lead to significant memory
+consumption.
+
+To avoid this, decimals with more than 200 digits are now returned as is.
+
 Bugfixes
 ========
 
diff --git a/docs/releases/5.0.8.txt b/docs/releases/5.0.8.txt
index 704ecf2c61..c371e4af0b 100644
--- a/docs/releases/5.0.8.txt
+++ b/docs/releases/5.0.8.txt
@@ -7,6 +7,15 @@ Django 5.0.8 release notes
 Django 5.0.8 fixes three security issues with severity "moderate", one security
 issue with severity "high", and several bugs in 5.0.7.
 
+CVE-2024-41989: Memory exhaustion in ``django.utils.numberformat.floatformat()``
+================================================================================
+
+If :tfilter:`floatformat` received a string representation of a number in
+scientific notation with a large exponent, it could lead to significant memory
+consumption.
+
+To avoid this, decimals with more than 200 digits are now returned as is.
+
 Bugfixes
 ========
 
diff --git a/tests/template_tests/filter_tests/test_floatformat.py b/tests/template_tests/filter_tests/test_floatformat.py
index 145858b75f..3d6c34a552 100644
--- a/tests/template_tests/filter_tests/test_floatformat.py
+++ b/tests/template_tests/filter_tests/test_floatformat.py
@@ -73,6 +73,7 @@ class FunctionTests(SimpleTestCase):
         self.assertEqual(floatformat(1.5e-15, 20), "0.00000000000000150000")
         self.assertEqual(floatformat(1.5e-15, -20), "0.00000000000000150000")
         self.assertEqual(floatformat(1.00000000000000015, 16), "1.0000000000000002")
+        self.assertEqual(floatformat("1e199"), "1" + "0" * 199)
 
     def test_invalid_inputs(self):
         cases = [
@@ -169,6 +170,22 @@ class FunctionTests(SimpleTestCase):
         self.assertEqual(floatformat(pos_inf), "inf")
         self.assertEqual(floatformat(neg_inf), "-inf")
         self.assertEqual(floatformat(pos_inf / pos_inf), "nan")
+        self.assertEqual(floatformat("inf"), "inf")
+        self.assertEqual(floatformat("NaN"), "NaN")
+
+    def test_too_many_digits_to_render(self):
+        cases = [
+            "1e200",
+            "1E200",
+            "1E10000000000000000",
+            "-1E10000000000000000",
+            "1e10000000000000000",
+            "-1e10000000000000000",
+            "1" + "0" * 1_000_000,
+        ]
+        for value in cases:
+            with self.subTest(value=value):
+                self.assertEqual(floatformat(value), value)
 
     def test_float_dunder_method(self):
         class FloatWrapper: