From af026fdd3ce08c4865467fe3343dc8d7360af8da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:06:09 +1100 Subject: [PATCH 1/5] Added decompression bomb check to ImageFont.getmask() --- src/PIL/ImageFont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7f0366ddb5d..78c6b9ecacd 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -150,6 +150,7 @@ def getmask(self, text, mode="", *args, **kwargs): :py:mod:`PIL.Image.core` interface module. """ _string_length_check(text) + Image._decompression_bomb_check(self.font.getsize(text)) return self.font.getmask(text, mode) def getbbox(self, text, *args, **kwargs): From 8676cbd4e7adafc0b5972ab51fed7b06ab8719d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:13:24 +1100 Subject: [PATCH 2/5] Do not try and crop glyphs from outside of source ImageFont image --- docs/releasenotes/10.2.0.rst | 10 ++++++++++ src/_imaging.c | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 0244b6f7792..4e7c587568e 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -77,6 +77,16 @@ Pillow will now raise a :py:exc:`ValueError` if the number of characters passed 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``. +A decompression bomb check has also been added to +:py:meth:`PIL.ImageFont.ImageFont.getmask`. + +ImageFont.getmask: Trim glyph size +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using PIL fonts, +:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that +they do not extend beyond the bitmap image. + ImageMath.eval: Restricted environment keys ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/_imaging.c b/src/_imaging.c index 2270c77fe7e..e06780c7563 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2649,6 +2649,18 @@ _font_new(PyObject *self_, PyObject *args) { self->glyphs[i].sy0 = S16(B16(glyphdata, 14)); self->glyphs[i].sx1 = S16(B16(glyphdata, 16)); self->glyphs[i].sy1 = S16(B16(glyphdata, 18)); + + // Do not allow glyphs to extend beyond bitmap image + // Helps prevent DOS by stopping cropped images being larger than the original + if (self->glyphs[i].sx1 > self->bitmap->xsize) { + self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize; + self->glyphs[i].sx1 = self->bitmap->xsize; + } + if (self->glyphs[i].sy1 > self->bitmap->ysize) { + self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize; + self->glyphs[i].sy1 = self->bitmap->ysize; + } + if (self->glyphs[i].dy0 < y0) { y0 = self->glyphs[i].dy0; } From ecd3948b45ca9c4c2e8b0b3cb58fe6af9798cfa2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:03:43 +1100 Subject: [PATCH 3/5] Test PILfont even when FreeType is supported --- Tests/test_imagefontpil.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 21b4dee3c4a..fd07ee23b13 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,14 +1,22 @@ from __future__ import annotations +import struct import pytest +from io import BytesIO -from PIL import Image, ImageDraw, ImageFont, features +from PIL import Image, ImageDraw, ImageFont, features, _util from .helper import assert_image_equal_tofile -pytestmark = pytest.mark.skipif( - features.check_module("freetype2"), - reason="PILfont superseded if FreeType is supported", -) +original_core = ImageFont.core + + +def setup_module(): + if features.check_module("freetype2"): + ImageFont.core = _util.DeferredError(ImportError) + + +def teardown_module(): + ImageFont.core = original_core def test_default_font(): @@ -44,3 +52,23 @@ def test_textbbox(): default_font = ImageFont.load_default() assert d.textlength("test", font=default_font) == 24 assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) + + +def test_decompression_bomb(): + glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (256, 256))) + with pytest.raises(Image.DecompressionBombError): + font.getmask("A" * 1_000_000) + + +@pytest.mark.timeout(4) +def test_oom(): + glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 32767, 32767, 0, 0, 32767, 32767) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (1, 1))) + font.getmask("A" * 1_000_000) From 6cad0d62e7020e80fe706f7a239a2ec588f6f1b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:14:45 +1100 Subject: [PATCH 4/5] Do not crop again if glyph is the same as the previous one --- src/_imaging.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index e06780c7563..8b5cac180bc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2733,7 +2733,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) { static PyObject * _font_getmask(ImagingFontObject *self, PyObject *args) { Imaging im; - Imaging bitmap; + Imaging bitmap = NULL; int x, b; int i = 0; int status; @@ -2765,10 +2765,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { b = self->baseline; for (x = 0; text[i]; i++) { glyph = &self->glyphs[text[i]]; - bitmap = - ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); - if (!bitmap) { - goto failed; + if (i == 0 || text[i] != text[i - 1]) { + ImagingDelete(bitmap); + bitmap = + ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); + if (!bitmap) { + goto failed; + } } status = ImagingPaste( im, @@ -2778,17 +2781,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { glyph->dy0 + b, glyph->dx1 + x, glyph->dy1 + b); - ImagingDelete(bitmap); if (status < 0) { goto failed; } x = x + glyph->dx; b = b + glyph->dy; } + ImagingDelete(bitmap); free(text); return PyImagingNew(im); failed: + ImagingDelete(bitmap); free(text); ImagingDelete(im); Py_RETURN_NONE; From 492e5b0e0aa36e1d2d9a0d71280a9e1449b261dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 16:58:03 +1100 Subject: [PATCH 5/5] Do not set default value for unused variable --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 8b5cac180bc..e0e5f804ad0 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2742,7 +2742,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { PyObject *encoded_string; unsigned char *text; - char *mode = ""; + char *mode; if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) { return NULL;