From 1fe1bb49c452b0318cad12ea9d97c3bef188e9a7 Mon Sep 17 00:00:00 2001
From: Andrew Murray <radarhere@users.noreply.github.com>
Date: Fri, 30 Jun 2023 23:32:26 +1000
Subject: [PATCH] Added ImageFont.MAX_STRING_LENGTH

---
 Tests/test_imagefont.py      | 21 +++++++++++++++++++++
 docs/reference/ImageFont.rst | 18 ++++++++++++++++++
 src/PIL/ImageFont.py         | 16 ++++++++++++++++
 3 files changed, 55 insertions(+)

diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 0d423aa..5afa0bc 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -990,6 +990,27 @@ class TestImageFont:
 
         assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
 
+    def test_too_many_characters(self):
+        font = self.get_font()
+        with pytest.raises(ValueError):
+            font.getlength("A" * 1000001)
+        with pytest.raises(ValueError):
+            font.getbbox("A" * 1000001)
+        with pytest.raises(ValueError):
+            font.getsize("A" * 1000001)
+        with pytest.raises(ValueError):
+            font.getoffset("A" * 1000001)
+        with pytest.raises(ValueError):
+            font.getmask2("A" * 1000001)
+    
+        transposed_font = ImageFont.TransposedFont(font)
+        with pytest.raises(ValueError):
+            transposed_font.getsize("A" * 1000001)
+    
+        default_font = ImageFont.load_default()
+        with pytest.raises(ValueError):
+            default_font.getsize("A" * 1000001)
+
 
 @skip_unless_feature("raqm")
 class TestImageFont_RaqmLayout(TestImageFont):
diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst
index 5f718ce..12edaf9 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -18,6 +18,15 @@ 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
 -------
 
@@ -72,3 +81,12 @@ Constants
 
     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/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 805c8ff..e0b7167 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -43,12 +43,21 @@ class _imagingft_not_installed:
         raise ImportError("The _imagingft C module is not installed")
 
 
+MAX_STRING_LENGTH = 1000000
+
+
 try:
     from . import _imagingft as core
 except ImportError:
     core = _imagingft_not_installed()
 
 
+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)
 
 # --------------------------------------------------------------------
@@ -125,6 +134,7 @@ class ImageFont:
 
         :return: (width, height)
         """
+        _string_length_check(text)
         return self.font.getsize(text)
 
     def getmask(self, text, mode="", *args, **kwargs):
@@ -297,6 +307,7 @@ class FreeTypeFont:
 
         :return: Width for horizontal, height for vertical text.
         """
+        _string_length_check(text)
         return self.font.getlength(text, mode, direction, features, language) / 64
 
     def getbbox(
@@ -356,6 +367,7 @@ class FreeTypeFont:
 
         :return: ``(left, top, right, bottom)`` bounding box
         """
+        _string_length_check(text)
         size, offset = self.font.getsize(
             text, mode, direction, features, language, anchor
         )
@@ -418,6 +430,7 @@ class FreeTypeFont:
         """
         # vertical offset is added for historical reasons
         # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929
+        _string_length_check(text)
         size, offset = self.font.getsize(text, "L", direction, features, language)
         return (
             size[0] + stroke_width * 2,
@@ -494,6 +507,7 @@ class FreeTypeFont:
 
         :return: A tuple of the x and y offset
         """
+        _string_length_check(text)
         return self.font.getsize(text)[1]
 
     def getmask(
@@ -655,6 +669,7 @@ class FreeTypeFont:
                  :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)
         size, offset = self.font.getsize(
             text, mode, direction, features, language, anchor
         )
@@ -758,6 +773,7 @@ class TransposedFont:
         self.orientation = orientation  # any 'transpose' argument, or None
 
     def getsize(self, text, *args, **kwargs):
+        _string_length_check(text)
         w, h = self.font.getsize(text)
         if self.orientation in (Image.ROTATE_90, Image.ROTATE_270):
             return h, w
-- 
2.33.0

