Skip to content

Commit

Permalink
Merge pull request #7669 from radarhere/imagefont_mask
Browse files Browse the repository at this point in the history
Do not try and crop glyphs from outside of source ImageFont image
  • Loading branch information
radarhere committed Jan 1, 2024
2 parents 4f17b60 + 492e5b0 commit 10c2df5
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 12 deletions.
38 changes: 33 additions & 5 deletions 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():
Expand Down Expand Up @@ -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)
10 changes: 10 additions & 0 deletions docs/releasenotes/10.2.0.rst
Expand Up @@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
1 change: 1 addition & 0 deletions src/PIL/ImageFont.py
Expand Up @@ -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):
Expand Down
30 changes: 23 additions & 7 deletions src/_imaging.c
Expand Up @@ -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;
}
Expand Down Expand Up @@ -2721,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;
Expand All @@ -2730,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;
Expand All @@ -2753,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,
Expand All @@ -2766,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;
Expand Down

0 comments on commit 10c2df5

Please sign in to comment.