Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ def test_open_formats(self) -> None:
assert im.mode == "RGB"
assert im.size == (128, 128)

@pytest.mark.parametrize("formats", (("!PNG",), ("PNG", "!PNG"), ("JPEG", "!PNG")))
def test_open_formats_exclude(self, formats: tuple[str]) -> None:
with Image.open("Tests/images/hopper.jpg", formats=formats):
pass

with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/hopper.png", formats=formats):
pass

def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True)

Expand Down
4 changes: 2 additions & 2 deletions docs/handbook/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ The following mitigations are listed in priority order.
advisories <https://github.com/python-pillow/Pillow/security/advisories>`_.
5. **Enforce** ``MAX_IMAGE_PIXELS`` — never set it to ``None``; treat
``Image.DecompressionBombWarning`` as an error.
6. **Allowlist image formats** — restrict accepted formats when opening
images, for example with ``Image.open(..., formats=...)``, and isolate
6. **Restrict image formats** — restrict formats when opening images, for
example with ``Image.open(..., formats=...)``, and isolate
installs/environments if you need to minimise supported formats.
7. **Strip metadata on output** — never pass through EXIF/XMP/ICC from user
uploads to publicly served images.
Expand Down
69 changes: 42 additions & 27 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -3595,11 +3595,17 @@ def open(
and be opened in binary mode. The file object will also seek to zero
before reading.
:param mode: The mode. If given, this argument must be "r".
:param formats: A list or tuple of formats to attempt to load the file in.
This can be used to restrict the set of formats checked.
Pass ``None`` to try all supported formats. You can print the set of
available formats by running ``python3 -m PIL`` or using
the :py:func:`PIL.features.pilinfo` function.
:param formats: A list or tuple of formats to attempt to load the file in, for
example, ``("JPEG", "GIF")``. This can be used to restrict the set of formats
checked.

To exclude a format, start the format with "!", for example,
``("!EPS", "!PSD")``.

Pass ``None`` to try all supported formats.

You can print the set of available formats by running ``python3 -m PIL`` or
using the :py:func:`PIL.features.pilinfo` function.
:returns: An :py:class:`~PIL.Image.Image` object.
:exception FileNotFoundError: If the file cannot be found.
:exception PIL.UnidentifiedImageError: If the image cannot be opened and
Expand All @@ -3619,11 +3625,20 @@ def open(
)
raise ValueError(msg)

if formats is None:
formats = ID
elif not isinstance(formats, (list, tuple)):
msg = "formats must be a list or tuple" # type: ignore[unreachable]
raise TypeError(msg)
if formats is not None:
if not isinstance(formats, (list, tuple)):
msg = "formats must be a list or tuple" # type: ignore[unreachable]
raise TypeError(msg)

allowed = set()
excluded = set()
for f in formats:
f = f.upper()
if f.startswith("!"):
excluded.add(f[1:])
else:
allowed.add(f)
allowed -= excluded

exclusive_fp = False
filename: str | bytes = ""
Expand Down Expand Up @@ -3654,12 +3669,15 @@ def _open_core(
fp: IO[bytes],
filename: str | bytes,
prefix: bytes,
formats: list[str] | tuple[str, ...],
check_formats: list[str],
) -> ImageFile.ImageFile | None:
for i in formats:
i = i.upper()
if i not in OPEN:
init()
if formats is not None:
if allowed:
check_formats = [f for f in check_formats if f in allowed]
else:
check_formats = [f for f in check_formats if f not in excluded]

for i in check_formats:
try:
factory, accept = OPEN[i]
result = not accept or accept(prefix)
Expand All @@ -3670,7 +3688,12 @@ def _open_core(
im = factory(fp, filename)
_decompression_bomb_check(im.size)
return im
except (SyntaxError, IndexError, TypeError, struct.error) as e:
except ( # noqa: PERF203
SyntaxError,
IndexError,
TypeError,
struct.error,
) as e:
if WARN_POSSIBLE_FORMATS:
warning_messages.append(i + " opening failed. " + str(e))
except BaseException:
Expand All @@ -3679,21 +3702,13 @@ def _open_core(
raise
return None

im = _open_core(fp, filename, prefix, formats)

if im is None and formats is ID:
if not (im := _open_core(fp, filename, prefix, ID)):
# Try preinit (few common plugins) then init (all plugins)
for loader in (preinit, init):
checked_formats = ID.copy()
loader()
if formats != checked_formats:
im = _open_core(
fp,
filename,
prefix,
tuple(f for f in formats if f not in checked_formats),
)
if im is not None:
if check_formats := [f for f in ID if f not in checked_formats]:
if im := _open_core(fp, filename, prefix, check_formats):
break

if im:
Expand Down
Loading