Repository: python-pillow/Pillow

Issue:
Load a restricted subset of image formats
Image parsing libraries are, and always have been, complicated pieces of software, with a requirement to (a) handle complicated input formats with a vast number of edge cases, and (b) operate as quickly and efficiently as possible.

This is a combination that leads to a lot of security bugs.

Pillow is no exception to this rule.

In many cases, when building an application, it makes sense to reduce the possible set of security bugs by only handling the input formats that you actually need to handle.

For example, if I only need to handle JPEG files, there is no reason for me to want to run a PostScript interpreter.  If I can design my program so it is *impossible* for the PostScript interpreter to be invoked, no matter what garbage input my program receives, then there's a huge category of security issues I don't need to worry about.

So, if I know what format the input file uses, is there a supported way to load it using *only* the required plugin?

There doesn't appear to be any documented way of doing this with Pillow.  As far as I can see, the only documented way of reading an image file is to use PIL.Image.open.  And if the input is malformed, then PIL.Image.open will potentially invoke *any* of the available format plugins, and will do so in an unpredictable order (depending on whether PIL.Image.init() or PIL.Image.preinit() is invoked first.)

An *undocumented* way would be to do this:
```python
PIL.Image.init()
image = PIL.Image.OPEN['JPEG'][0](fp, filename)
```
But this is pretty cryptic and I wouldn't assume this will always work with future Pillow versions.  This also skips the _decompression_bomb_check that is performed by PIL.Image.open.



Discussion:
No, there isn't a documented way of doing this at the moment.

Here is my suggestion.
```python
import PIL.Image
PIL.Image.init()
PIL.Image.ID = ['JPEG']
im = PIL.Image.open("Tests/images/hopper.jpg")
```
I've created #4833 as a suggested new API for this functionality.

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -2837,7 +2837,7 @@ def _decompression_bomb_check(size):
         )
 
 
-def open(fp, mode="r"):
+def open(fp, mode="r", formats=None):
     """
     Opens and identifies the given image file.
 
@@ -2852,12 +2852,18 @@ def open(fp, mode="r"):
        ``file.seek``, and ``file.tell`` methods,
        and be opened in binary mode.
     :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 ``python -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
        identified.
     :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO``
        instance is used for ``fp``.
+    :exception TypeError: If ``formats`` is not ``None``, a list or a tuple.
     """
 
     if mode != "r":
@@ -2868,6 +2874,11 @@ def open(fp, mode="r"):
             "Binary data must be used instead."
         )
 
+    if formats is None:
+        formats = ID
+    elif not isinstance(formats, (list, tuple)):
+        raise TypeError("formats must be a list or tuple")
+
     exclusive_fp = False
     filename = ""
     if isinstance(fp, Path):
@@ -2891,8 +2902,8 @@ def open(fp, mode="r"):
 
     accept_warnings = []
 
-    def _open_core(fp, filename, prefix):
-        for i in ID:
+    def _open_core(fp, filename, prefix, formats):
+        for i in formats:
             try:
                 factory, accept = OPEN[i]
                 result = not accept or accept(prefix)
@@ -2914,11 +2925,11 @@ def _open_core(fp, filename, prefix):
                 raise
         return None
 
-    im = _open_core(fp, filename, prefix)
+    im = _open_core(fp, filename, prefix, formats)
 
     if im is None:
         if init():
-            im = _open_core(fp, filename, prefix)
+            im = _open_core(fp, filename, prefix, formats)
 
     if im:
         im._exclusive_fp = exclusive_fp


Test patch:
diff --git a/Tests/test_image.py b/Tests/test_image.py
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -87,6 +87,26 @@ def test_sanity(self):
         # with pytest.raises(MemoryError):
         #   Image.new("L", (1000000, 1000000))
 
+    def test_open_formats(self):
+        PNGFILE = "Tests/images/hopper.png"
+        JPGFILE = "Tests/images/hopper.jpg"
+
+        with pytest.raises(TypeError):
+            Image.open(PNGFILE, formats=123)
+
+        for formats in [["JPEG"], ("JPEG",)]:
+            with pytest.raises(UnidentifiedImageError):
+                Image.open(PNGFILE, formats=formats)
+
+            with Image.open(JPGFILE, formats=formats) as im:
+                assert im.mode == "RGB"
+                assert im.size == (128, 128)
+
+        for file in [PNGFILE, JPGFILE]:
+            with Image.open(file, formats=None) as im:
+                assert im.mode == "RGB"
+                assert im.size == (128, 128)
+
     def test_width_height(self):
         im = Image.new("RGB", (1, 2))
         assert im.width == 1