CVE-2023-44271: Allocation of Resources Without Limits or Throttling (CWE-770)
value: An issue was discovered in Pillow before 10.0.0. It is a Denial of Service that uncontrollably allocates memory to process a given task, potentially causing a service to crash by having it run out of memory. This occurs for truetype in ImageFont when textlength in an ImageDraw instance operates on a long text argument.
CWE description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 7.5
Published: 2023-11-03T05:15Z
Repository: https://github.com/python-pillow/Pillow
Language: Other
Files changed: Tests/test_imagefont.py, docs/reference/ImageFont.rst, docs/releasenotes/10.0.0.rst, src/PIL/ImageFont.py
Commit date: 2023-06-30 23:32:26 +1000
Commit message: Added ImageFont.MAX_STRING_LENGTH

Diff: diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 7fa8ff8cbf..c50447a153 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -1036,10 +1036,29 @@ def test_render_mono_size():
 
     draw.text((10, 10), "r" * 10, "black", ttf)
     assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
 
 
+def test_too_many_characters(font):
+    with pytest.raises(ValueError):
+        font.getlength("A" * 1000001)
+    with pytest.raises(ValueError):
+        font.getbbox("A" * 1000001)
+    with pytest.raises(ValueError):
+        font.getmask2("A" * 1000001)
+
+    transposed_font = ImageFont.TransposedFont(font)
+    with pytest.raises(ValueError):
+        transposed_font.getlength("A" * 1000001)
+
+    default_font = ImageFont.load_default()
+    with pytest.raises(ValueError):
+        default_font.getlength("A" * 1000001)
+    with pytest.raises(ValueError):
+        default_font.getbbox("A" * 1000001)
+
+
 @pytest.mark.parametrize(
     "test_file",
     [
         "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf",
         "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst
index 946bd3c4be..2abfa0cc99 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -16,10 +16,19 @@ PCF font descriptors (X window font formats) to this format.
 Starting with version 1.1.4, PIL can be configured to support TrueType and
 OpenType fonts (as well as other font formats supported by the FreeType
 library). For earlier versions, TrueType support is only available as part of
 the imToolkit package.
 
+.. warning::
+    To protect against potential DOS attacks when using arbitrary strings as
+    text input, Pillow will raise a ``ValueError`` if the number of characters
+    is over a certain limit, :py:data:`MAX_STRING_LENGTH`.
+
+    This threshold can be changed by setting
+    :py:data:`MAX_STRING_LENGTH`. It can be disabled by setting
+    ``ImageFont.MAX_STRING_LENGTH = None``.
+
 Example
 -------
 
 ::
 
@@ -71,5 +80,14 @@ Constants
     Use Raqm text layout for TrueType font.
     Advanced features are supported.
 
     Requires Raqm, you can check support using
     :py:func:`PIL.features.check_feature` with ``feature="raqm"``.
+
+Constants
+---------
+
+.. data:: MAX_STRING_LENGTH
+
+    Set to 1,000,000, to protect against potential DOS attacks. Pillow will
+    raise a ``ValueError`` if the number of characters is over this limit. The
+    check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index 94ff04d46a..4cd6293229 100644
--- a/docs/releasenotes/10.0.0.rst
+++ b/docs/releasenotes/10.0.0.rst
@@ -168,10 +168,22 @@ excessive width and zero height, or zero width and excessive height. That has
 now been fixed.
 
 This effectively dates to the PIL fork, since problem images would still have
 been processed before Pillow started checking for decompression bombs.
 
+Added ImageFont.MAX_STRING_LENGTH
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To protect against potential DOS attacks when using arbitrary strings as text
+input, Pillow will now raise a ``ValueError`` if the number of characters
+passed into ImageFont methods is over a certain limit,
+:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.
+
+This threshold can be changed by setting
+:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting
+``ImageFont.MAX_STRING_LENGTH = None``.
+
 Other Changes
 =============
 
 32-bit wheels
 ^^^^^^^^^^^^^
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 3ddc1aaad6..1030985ebc 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -39,18 +39,27 @@ from ._util import is_directory, is_path
 class Layout(IntEnum):
     BASIC = 0
     RAQM = 1
 
 
+MAX_STRING_LENGTH = 1000000
+
+
 try:
     from . import _imagingft as core
 except ImportError as ex:
     from ._util import DeferredError
 
     core = DeferredError(ex)
 
 
+def _string_length_check(text):
+    if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
+        msg = "too many characters in string"
+        raise ValueError(msg)
+
+
 # FIXME: add support for pilfont2 format (see FontFile.py)
 
 # --------------------------------------------------------------------
 # Font metrics format:
 #       "PILfont" LF
@@ -150,20 +159,22 @@ class ImageFont:
                      mode. Note that the mode is always a string, to simplify
                      C-level implementations.
 
         :return: ``(left, top, right, bottom)`` bounding box
         """
+        _string_length_check(text)
         width, height = self.font.getsize(text)
         return 0, 0, width, height
 
     def getlength(self, text, *args, **kwargs):
         """
         Returns length (in pixels) of given text.
         This is the amount by which following text should be offset.
 
         .. versionadded:: 9.2.0
         """
+        _string_length_check(text)
         width, height = self.font.getsize(text)
         return width
 
 
 ##
@@ -307,10 +318,11 @@ class FreeTypeFont:
                          <https://www.w3.org/International/articles/language-tags/>`_
                          Requires libraqm.
 
         :return: Width for horizontal, height for vertical text.
         """
+        _string_length_check(text)
         return self.font.getlength(text, mode, direction, features, language) / 64
 
     def getbbox(
         self,
         text,
@@ -366,10 +378,11 @@ class FreeTypeFont:
                         the anchor to the text. The default alignment is top left.
                         See :ref:`text-anchors` for valid values.
 
         :return: ``(left, top, right, bottom)`` bounding box
         """
+        _string_length_check(text)
         size, offset = self.font.getsize(
             text, mode, direction, features, language, anchor
         )
         left, top = offset[0] - stroke_width, offset[1] - stroke_width
         width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width
@@ -544,10 +557,11 @@ class FreeTypeFont:
 
         :return: A tuple of an internal PIL storage memory instance as defined by the
                  :py:mod:`PIL.Image.core` interface module, and the text offset, the
                  gap between the starting coordinate and the first marking
         """
+        _string_length_check(text)
         if start is None:
             start = (0, 0)
         im, size, offset = self.font.render(
             text,
             Image.core.fill,
@@ -682,10 +696,11 @@ class TransposedFont:
 
     def getlength(self, text, *args, **kwargs):
         if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
             msg = "text length is undefined for text rotated by 90 or 270 degrees"
             raise ValueError(msg)
+        _string_length_check(text)
         return self.font.getlength(text, *args, **kwargs)
 
 
 def load(filename):
     """

Fixed code: def test_too_many_characters(font):
    with pytest.raises(ValueError):
        font.getlength("A" * 1000001)
    with pytest.raises(ValueError):
        font.getbbox("A" * 1000001)
    with pytest.raises(ValueError):
        font.getmask2("A" * 1000001)

    transposed_font = ImageFont.TransposedFont(font)
    with pytest.raises(ValueError):
        transposed_font.getlength("A" * 1000001)

    default_font = ImageFont.load_default()
    with pytest.raises(ValueError):
        default_font.getlength("A" * 1000001)
    with pytest.raises(ValueError):
        default_font.getbbox("A" * 1000001)


.. warning::
    To protect against potential DOS attacks when using arbitrary strings as
    text input, Pillow will raise a ``ValueError`` if the number of characters
    is over a certain limit, :py:data:`MAX_STRING_LENGTH`.

    This threshold can be changed by setting
    :py:data:`MAX_STRING_LENGTH`. It can be disabled by setting
    ``ImageFont.MAX_STRING_LENGTH = None``.


Constants
---------

.. data:: MAX_STRING_LENGTH

    Set to 1,000,000, to protect against potential DOS attacks. Pillow will
    raise a ``ValueError`` if the number of characters is over this limit. The
    check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
Added ImageFont.MAX_STRING_LENGTH
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

To protect against potential DOS attacks when using arbitrary strings as text
input, Pillow will now raise a ``ValueError`` if the number of characters
passed into ImageFont methods is over a certain limit,
:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.

This threshold can be changed by setting
:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting
``ImageFont.MAX_STRING_LENGTH = None``.

MAX_STRING_LENGTH = 1000000


def _string_length_check(text):
    if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
        msg = "too many characters in string"
        raise ValueError(msg)


        _string_length_check(text)
        _string_length_check(text)
        _string_length_check(text)
        _string_length_check(text)
        _string_length_check(text)
        _string_length_check(text)