diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index da41b3a12..07e5b96c5 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,4 +1,5 @@ from __future__ import print_function +import pytest from PIL import Image, ImageMath @@ -56,6 +57,10 @@ class TestImageMath(PillowTestCase): pixel(ImageMath.eval("float(B)**33", images)), "F 8589934592.0" ) + def test_prevent_exec(self): + with pytest.raises(ValueError): + ImageMath.eval("exec('pass')") + def test_logical(self): self.assertEqual(pixel(ImageMath.eval("not A", images)), 0) self.assertEqual(pixel(ImageMath.eval("A and B", images)), "L 2") diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 2f2620b74..6b2600a49 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,4 +1,8 @@ from PIL import Image, ImageShow +from tempfile import mkdtemp +from os import rmdir +from os.path import join +from shutil import rmtree from .helper import PillowTestCase, hopper, on_ci, unittest @@ -50,3 +54,11 @@ class TestImageShow(PillowTestCase): def test_viewers(self): for viewer in ImageShow._viewers: viewer.get_command("test.jpg") + + def test_file_deprecated(self): + tmp_path = mkdtemp() + f = join(tmp_path, "temp.jpg") + for viewer in ImageShow._viewers: + hopper().save(f) + viewer.show_file(file=f) + # viewer.show_file() diff --git a/docs/releasenotes/6.2.2.3.rst b/docs/releasenotes/6.2.2.3.rst new file mode 100644 index 000000000..75f07b11b --- /dev/null +++ b/docs/releasenotes/6.2.2.3.rst @@ -0,0 +1,21 @@ +6.2.2.3 +------- + +Security +======== + +This release addresses several critical CVEs. + +restrict builtins available to ImageMath.eval +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CVE-2022-22817 Restrict builtins for ImageMath.eval() + +To limit :py:class:`PIL.ImageMath` to working with images, Pillow will now restrict the +builtins available to :py:meth:`PIL.ImageMath.eval`. This will help prevent problems +arising if users evaluate arbitrary expressions, such as +``ImageMath.eval("exec(exit())")``. + +CVE-2022-24303 Pillow before 9.0.1 allows attackers to delete files because spaces in temporary pathnames are mishandled. + +A bunch of changes related to temporary files and race conditions are fixed diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 392151c10..7f498d00e 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -264,7 +264,13 @@ def eval(expression, _dict={}, **kw): if hasattr(v, "im"): args[k] = _Operand(v) - out = builtins.eval(expression, args) + # out = builtins.eval(expression, args) + code = compile(expression, "", "eval") + for name in code.co_names: + if name not in args and name != "abs": + raise ValueError(f"'{name}' not allowed") + + out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) try: return out.im except AttributeError: diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index ca622c525..9ab168464 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -17,7 +17,6 @@ from __future__ import print_function import os import subprocess import sys -import tempfile from PIL import Image @@ -98,6 +97,15 @@ class Viewer(object): os.system(self.get_command(file, **options)) return 1 + def _remove_path_after_delay(self, path): + subprocess.Popen( + [ + sys.executable, + "-c", + "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", + path, + ] + ) # -------------------------------------------------------------------- @@ -136,16 +144,9 @@ elif sys.platform == "darwin": def show_file(self, file, **options): """Display given file""" - fd, path = tempfile.mkstemp() - with os.fdopen(fd, "w") as f: - f.write(file) - with open(path, "r") as f: - subprocess.Popen( - ["im=$(cat); open -a Preview.app $im; sleep 20; rm -f $im"], - shell=True, - stdin=f, - ) - os.remove(path) + + subprocess.call(["open", "-a", "Preview.app", file]) + self._remove_path_after_delay(file) return 1 register(MacViewer) @@ -168,49 +169,49 @@ else: format = "PNG" options = {"compress_level": 1} - def get_command(self, file, **options): - command = self.get_command_ex(file, **options)[0] - return "(%s %s; rm -f %s)&" % (command, quote(file), quote(file)) + def get_command(self,file,**options): + return " ".join(self.get_command_ex(file,options=options)) + + def get_command_ex(self, file, **options): + return ["display",file] def show_file(self, file, **options): - """Display given file""" - fd, path = tempfile.mkstemp() - with os.fdopen(fd, "w") as f: - f.write(file) - with open(path, "r") as f: - command = self.get_command_ex(file, **options)[0] - subprocess.Popen( - ["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f - ) - os.remove(path) + """ + Display given file. + """ + args = self.get_command_ex(file,**options) + subprocess.Popen(args) + + self._remove_path_after_delay(file) return 1 # implementations class DisplayViewer(UnixViewer): def get_command_ex(self, file, **options): - command = executable = "display" - return command, executable + return ["display", file] if which("display"): register(DisplayViewer) class EogViewer(UnixViewer): def get_command_ex(self, file, **options): - command = executable = "eog" - return command, executable + return ["eog", "-n", file] if which("eog"): register(EogViewer) class XVViewer(UnixViewer): - def get_command_ex(self, file, title=None, **options): + def get_command_ex(self, file, **options): # note: xv is pretty outdated. most modern systems have # imagemagick's display command instead. - command = executable = "xv" - if title: - command += " -name %s" % quote(title) - return command, executable + args = ["xv"] + if options.get("title") is not None: + args.append("-name") + args.append(options.get("title")) + args.append(file) + return args + if which("xv"): register(XVViewer)