Repository: python-pillow/Pillow

Issue:
Provide a documented way to turn off EPS handling via Ghostscript
Pillow parses EPS files through ghostscript.

Ghostscript is troubled with regular security issues of critical impact, such as remote code execution. There has been [this one](https://github.com/farisv/PIL-RCE-Ghostscript-CVE-2018-16509), two CVEs this year already, and there is another one [just coming up](https://git.ghostscript.com/?p=ghostpdl.git;a=commit;h=e59216049cac290fb437a04c4f41ea46826cfba5) with the patch not yet included in any release.

We do not need EPS handling and we can turn it off in our code using `Image.open(…, formats=["JPG", "PNG", …])`. However, we use third-party components that also use Pillow (such as `reportlab`) and we'd like to make sure they also don't accidentally open EPS files and we can't modify the code.

From what I can tell, we can accomplish this with:

```python
from PIL import Image

if "EPS" in Image.ID:
    Image.ID.remove("EPS")
```

However, that seems to be undocumented and feels like unclean monkeypatching. I think it would be worth it providing a documented API to turn off third-party backends like ghostscript globally.


Discussion:

Resolving patch:
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -37,33 +37,39 @@
 split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
 field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
 
+gs_binary = None
 gs_windows_binary = None
-if sys.platform.startswith("win"):
-    import shutil
-
-    for binary in ("gswin32c", "gswin64c", "gs"):
-        if shutil.which(binary) is not None:
-            gs_windows_binary = binary
-            break
-    else:
-        gs_windows_binary = False
 
 
 def has_ghostscript():
-    if gs_windows_binary:
-        return True
-    if not sys.platform.startswith("win"):
-        try:
-            subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
-            return True
-        except OSError:
-            # No Ghostscript
-            pass
-    return False
+    global gs_binary, gs_windows_binary
+    if gs_binary is None:
+        if sys.platform.startswith("win"):
+            if gs_windows_binary is None:
+                import shutil
+
+                for binary in ("gswin32c", "gswin64c", "gs"):
+                    if shutil.which(binary) is not None:
+                        gs_windows_binary = binary
+                        break
+                else:
+                    gs_windows_binary = False
+            gs_binary = gs_windows_binary
+        else:
+            try:
+                subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
+                gs_binary = "gs"
+            except OSError:
+                gs_binary = False
+    return gs_binary is not False
 
 
 def Ghostscript(tile, size, fp, scale=1, transparency=False):
     """Render an image using Ghostscript"""
+    global gs_binary
+    if not has_ghostscript():
+        msg = "Unable to locate Ghostscript on paths"
+        raise OSError(msg)
 
     # Unpack decoder tile
     decoder, tile, offset, data = tile[0]
@@ -113,7 +119,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
 
     # Build Ghostscript command
     command = [
-        "gs",
+        gs_binary,
         "-q",  # quiet mode
         "-g%dx%d" % size,  # set output geometry (pixels)
         "-r%fx%f" % res,  # set input DPI (dots per inch)
@@ -132,19 +138,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
         "showpage",
     ]
 
-    if gs_windows_binary is not None:
-        if not gs_windows_binary:
-            try:
-                os.unlink(outfile)
-                if infile_temp:
-                    os.unlink(infile_temp)
-            except OSError:
-                pass
-
-            msg = "Unable to locate Ghostscript on paths"
-            raise OSError(msg)
-        command[0] = gs_windows_binary
-
     # push data through Ghostscript
     try:
         startupinfo = None


Test patch:
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -8,6 +8,7 @@
     assert_image_similar,
     assert_image_similar_tofile,
     hopper,
+    is_win32,
     mark_if_feature_version,
     skip_unless_feature,
 )
@@ -98,6 +99,20 @@ def test_load():
         assert im.load()[0, 0] == (255, 255, 255)
 
 
+def test_binary():
+    if HAS_GHOSTSCRIPT:
+        assert EpsImagePlugin.gs_binary is not None
+    else:
+        assert EpsImagePlugin.gs_binary is False
+
+    if not is_win32():
+        assert EpsImagePlugin.gs_windows_binary is None
+    elif not HAS_GHOSTSCRIPT:
+        assert EpsImagePlugin.gs_windows_binary is False
+    else:
+        assert EpsImagePlugin.gs_windows_binary is not None
+
+
 def test_invalid_file():
     invalid_file = "Tests/images/flower.jpg"
     with pytest.raises(SyntaxError):