Put CVE fix in for CVE-2022-22817 Restrict builtins for ImageMath.eval()

Put in fixes from CVE
Update release documentation

Ensure all tests pass as before
This commit is contained in:
Frederick Price 2023-02-15 09:55:13 -05:00
parent 538ac8d360
commit 1184cbf916
5 changed files with 79 additions and 34 deletions

View File

@ -1,4 +1,5 @@
from __future__ import print_function from __future__ import print_function
import pytest
from PIL import Image, ImageMath from PIL import Image, ImageMath
@ -56,6 +57,10 @@ class TestImageMath(PillowTestCase):
pixel(ImageMath.eval("float(B)**33", images)), "F 8589934592.0" 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): def test_logical(self):
self.assertEqual(pixel(ImageMath.eval("not A", images)), 0) self.assertEqual(pixel(ImageMath.eval("not A", images)), 0)
self.assertEqual(pixel(ImageMath.eval("A and B", images)), "L 2") self.assertEqual(pixel(ImageMath.eval("A and B", images)), "L 2")

View File

@ -1,4 +1,8 @@
from PIL import Image, ImageShow 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 from .helper import PillowTestCase, hopper, on_ci, unittest
@ -50,3 +54,11 @@ class TestImageShow(PillowTestCase):
def test_viewers(self): def test_viewers(self):
for viewer in ImageShow._viewers: for viewer in ImageShow._viewers:
viewer.get_command("test.jpg") 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()

View File

@ -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

View File

@ -264,7 +264,13 @@ def eval(expression, _dict={}, **kw):
if hasattr(v, "im"): if hasattr(v, "im"):
args[k] = _Operand(v) args[k] = _Operand(v)
out = builtins.eval(expression, args) # out = builtins.eval(expression, args)
code = compile(expression, "<string>", "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: try:
return out.im return out.im
except AttributeError: except AttributeError:

View File

@ -17,7 +17,6 @@ from __future__ import print_function
import os import os
import subprocess import subprocess
import sys import sys
import tempfile
from PIL import Image from PIL import Image
@ -98,6 +97,15 @@ class Viewer(object):
os.system(self.get_command(file, **options)) os.system(self.get_command(file, **options))
return 1 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): def show_file(self, file, **options):
"""Display given file""" """Display given file"""
fd, path = tempfile.mkstemp()
with os.fdopen(fd, "w") as f: subprocess.call(["open", "-a", "Preview.app", file])
f.write(file) self._remove_path_after_delay(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)
return 1 return 1
register(MacViewer) register(MacViewer)
@ -169,48 +170,48 @@ else:
options = {"compress_level": 1} options = {"compress_level": 1}
def get_command(self,file,**options): def get_command(self,file,**options):
command = self.get_command_ex(file, **options)[0] return " ".join(self.get_command_ex(file,options=options))
return "(%s %s; rm -f %s)&" % (command, quote(file), quote(file))
def get_command_ex(self, file, **options):
return ["display",file]
def show_file(self, file, **options): def show_file(self, file, **options):
"""Display given file""" """
fd, path = tempfile.mkstemp() Display given file.
with os.fdopen(fd, "w") as f: """
f.write(file) args = self.get_command_ex(file,**options)
with open(path, "r") as f: subprocess.Popen(args)
command = self.get_command_ex(file, **options)[0]
subprocess.Popen( self._remove_path_after_delay(file)
["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f
)
os.remove(path)
return 1 return 1
# implementations # implementations
class DisplayViewer(UnixViewer): class DisplayViewer(UnixViewer):
def get_command_ex(self, file, **options): def get_command_ex(self, file, **options):
command = executable = "display" return ["display", file]
return command, executable
if which("display"): if which("display"):
register(DisplayViewer) register(DisplayViewer)
class EogViewer(UnixViewer): class EogViewer(UnixViewer):
def get_command_ex(self, file, **options): def get_command_ex(self, file, **options):
command = executable = "eog" return ["eog", "-n", file]
return command, executable
if which("eog"): if which("eog"):
register(EogViewer) register(EogViewer)
class XVViewer(UnixViewer): 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 # note: xv is pretty outdated. most modern systems have
# imagemagick's display command instead. # imagemagick's display command instead.
command = executable = "xv" args = ["xv"]
if title: if options.get("title") is not None:
command += " -name %s" % quote(title) args.append("-name")
return command, executable args.append(options.get("title"))
args.append(file)
return args
if which("xv"): if which("xv"):
register(XVViewer) register(XVViewer)