diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 45aa80f56af..9d26e7ce9fd 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -59,6 +59,19 @@ def test_load_unsupported() -> None: WmfImagePlugin.WmfStubImageFile(b) +@pytest.mark.skipif(not hasattr(Image.core, "drawwmf"), reason="requires Windows") +def test_load_oversized_dimensions() -> None: + with Image.open("Tests/images/drawing.wmf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) + assert im.size == (82, 82) + + # (60000, 1) DPI produces a 68450x1 image + # width exceeds the 65535 limit of the 16-bit BITMAPCOREHEADER field + # in the native drawwmf handler, previously causing a heap over-read. + with pytest.raises(ValueError, match="invalid dimensions"): + im.load((60000, 1)) + + def test_render() -> None: with open("Tests/images/drawing.emf", "rb") as fp: data = fp.read() @@ -116,6 +129,17 @@ def test_load_set_dpi() -> None: assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) + for dpi in (0, (0, 0), (0, 1), (1, 0)): + with Image.open("Tests/images/drawing.wmf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) + im.load(dpi) + + for dpi in (-1, (-1, -1), (-1, 1), (1, -1)): + with Image.open("Tests/images/drawing.wmf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) + with pytest.raises(ValueError, match="DPI must be non-negative"): + im.load(dpi) + with Image.open("Tests/images/drawing.emf") as im: assert im.size == (1625, 1625) assert isinstance(im, WmfImagePlugin.WmfStubImageFile) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 15dccae93fa..742a98427ee 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -54,7 +54,7 @@ def _open(self) -> None: s = self.fp.read(44) if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): - # placeable windows metafile + # placeable Windows metafile # get units per inch inch = word(s, 14) @@ -120,13 +120,16 @@ def load( self, dpi: float | tuple[float, float] | None = None ) -> Image.core.PixelAccess | None: if dpi is not None: + xy_dpi = dpi if isinstance(dpi, tuple) else (dpi, dpi) + if any(v < 0 for v in xy_dpi): + msg = "DPI must be non-negative" + raise ValueError(msg) + self.info["dpi"] = dpi x0, y0, x1, y1 = self.info["wmf_bbox"] - if not isinstance(dpi, tuple): - dpi = dpi, dpi self._size = ( - int((x1 - x0) * dpi[0] / self._inch[0]), - int((y1 - y0) * dpi[1] / self._inch[1]), + int((x1 - x0) * xy_dpi[0] / self._inch[0]), + int((y1 - y0) * xy_dpi[1] / self._inch[1]), ) Image._decompression_bomb_check(self.size) return super().load() @@ -151,13 +154,15 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: if hasattr(Image.core, "drawwmf"): - # install default handler (windows only) + # install default handler (Windows only) class WmfHandler(ImageFile.StubHandler): def open(self, im: ImageFile.StubImageFile) -> None: self.bbox = im.info["wmf_bbox"] def load(self, im: ImageFile.StubImageFile) -> Image.Image: + if min(im.size) == 0: + return Image.new("RGB", im.size) assert im.fp is not None im.fp.seek(0) # rewind return Image.frombytes( diff --git a/src/display.c b/src/display.c index 56e76b68b47..f15236e438b 100644 --- a/src/display.c +++ b/src/display.c @@ -792,6 +792,16 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { return NULL; } + /* bcWidth/bcHeight in BITMAPCOREHEADER are 16-bit fields (WORD). If width + * or height do not fit, they would silently truncate when assigned below, + * while the later buffer read (PyBytes_FromStringAndSize) still uses the + * untruncated values, causing a heap buffer over-read. Reject anything + * that would not round-trip through a 16-bit unsigned field. */ + if (width <= 0 || height <= 0 || width > 65535 || height > 65535) { + PyErr_SetString(PyExc_ValueError, "invalid dimensions"); + return NULL; + } + /* step 1: copy metafile contents into METAFILE object */ if (datasize > 22 && GET32(data, 0) == 0x9ac6cdd7) {