Skip to content

Commit 10c2df5

Browse files
authored
Merge pull request #7669 from radarhere/imagefont_mask
Do not try and crop glyphs from outside of source ImageFont image
2 parents 4f17b60 + 492e5b0 commit 10c2df5

File tree

4 files changed

+67
-12
lines changed

4 files changed

+67
-12
lines changed

Tests/test_imagefontpil.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
from __future__ import annotations
2+
import struct
23
import pytest
4+
from io import BytesIO
35

4-
from PIL import Image, ImageDraw, ImageFont, features
6+
from PIL import Image, ImageDraw, ImageFont, features, _util
57

68
from .helper import assert_image_equal_tofile
79

8-
pytestmark = pytest.mark.skipif(
9-
features.check_module("freetype2"),
10-
reason="PILfont superseded if FreeType is supported",
11-
)
10+
original_core = ImageFont.core
11+
12+
13+
def setup_module():
14+
if features.check_module("freetype2"):
15+
ImageFont.core = _util.DeferredError(ImportError)
16+
17+
18+
def teardown_module():
19+
ImageFont.core = original_core
1220

1321

1422
def test_default_font():
@@ -44,3 +52,23 @@ def test_textbbox():
4452
default_font = ImageFont.load_default()
4553
assert d.textlength("test", font=default_font) == 24
4654
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
55+
56+
57+
def test_decompression_bomb():
58+
glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256)
59+
fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
60+
61+
font = ImageFont.ImageFont()
62+
font._load_pilfont_data(fp, Image.new("L", (256, 256)))
63+
with pytest.raises(Image.DecompressionBombError):
64+
font.getmask("A" * 1_000_000)
65+
66+
67+
@pytest.mark.timeout(4)
68+
def test_oom():
69+
glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 32767, 32767, 0, 0, 32767, 32767)
70+
fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
71+
72+
font = ImageFont.ImageFont()
73+
font._load_pilfont_data(fp, Image.new("L", (1, 1)))
74+
font.getmask("A" * 1_000_000)

docs/releasenotes/10.2.0.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ Pillow will now raise a :py:exc:`ValueError` if the number of characters passed
7777
This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It
7878
can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
7979

80+
A decompression bomb check has also been added to
81+
:py:meth:`PIL.ImageFont.ImageFont.getmask`.
82+
83+
ImageFont.getmask: Trim glyph size
84+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
85+
86+
To protect against potential DOS attacks when using PIL fonts,
87+
:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that
88+
they do not extend beyond the bitmap image.
89+
8090
ImageMath.eval: Restricted environment keys
8191
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8292

src/PIL/ImageFont.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def getmask(self, text, mode="", *args, **kwargs):
150150
:py:mod:`PIL.Image.core` interface module.
151151
"""
152152
_string_length_check(text)
153+
Image._decompression_bomb_check(self.font.getsize(text))
153154
return self.font.getmask(text, mode)
154155

155156
def getbbox(self, text, *args, **kwargs):

src/_imaging.c

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2649,6 +2649,18 @@ _font_new(PyObject *self_, PyObject *args) {
26492649
self->glyphs[i].sy0 = S16(B16(glyphdata, 14));
26502650
self->glyphs[i].sx1 = S16(B16(glyphdata, 16));
26512651
self->glyphs[i].sy1 = S16(B16(glyphdata, 18));
2652+
2653+
// Do not allow glyphs to extend beyond bitmap image
2654+
// Helps prevent DOS by stopping cropped images being larger than the original
2655+
if (self->glyphs[i].sx1 > self->bitmap->xsize) {
2656+
self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize;
2657+
self->glyphs[i].sx1 = self->bitmap->xsize;
2658+
}
2659+
if (self->glyphs[i].sy1 > self->bitmap->ysize) {
2660+
self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize;
2661+
self->glyphs[i].sy1 = self->bitmap->ysize;
2662+
}
2663+
26522664
if (self->glyphs[i].dy0 < y0) {
26532665
y0 = self->glyphs[i].dy0;
26542666
}
@@ -2721,7 +2733,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) {
27212733
static PyObject *
27222734
_font_getmask(ImagingFontObject *self, PyObject *args) {
27232735
Imaging im;
2724-
Imaging bitmap;
2736+
Imaging bitmap = NULL;
27252737
int x, b;
27262738
int i = 0;
27272739
int status;
@@ -2730,7 +2742,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
27302742
PyObject *encoded_string;
27312743

27322744
unsigned char *text;
2733-
char *mode = "";
2745+
char *mode;
27342746

27352747
if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) {
27362748
return NULL;
@@ -2753,10 +2765,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
27532765
b = self->baseline;
27542766
for (x = 0; text[i]; i++) {
27552767
glyph = &self->glyphs[text[i]];
2756-
bitmap =
2757-
ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
2758-
if (!bitmap) {
2759-
goto failed;
2768+
if (i == 0 || text[i] != text[i - 1]) {
2769+
ImagingDelete(bitmap);
2770+
bitmap =
2771+
ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
2772+
if (!bitmap) {
2773+
goto failed;
2774+
}
27602775
}
27612776
status = ImagingPaste(
27622777
im,
@@ -2766,17 +2781,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
27662781
glyph->dy0 + b,
27672782
glyph->dx1 + x,
27682783
glyph->dy1 + b);
2769-
ImagingDelete(bitmap);
27702784
if (status < 0) {
27712785
goto failed;
27722786
}
27732787
x = x + glyph->dx;
27742788
b = b + glyph->dy;
27752789
}
2790+
ImagingDelete(bitmap);
27762791
free(text);
27772792
return PyImagingNew(im);
27782793

27792794
failed:
2795+
ImagingDelete(bitmap);
27802796
free(text);
27812797
ImagingDelete(im);
27822798
Py_RETURN_NONE;

0 commit comments

Comments
 (0)