update 3.6.13 branch with rl_safe_eval & rl_config.toColorCanUse changes 3.6.13
authorrobin
Mon, 24 Apr 2023 13:52:40 +0100
branch3.6.13
changeset 4771 1c39d2db15bb
parent 4745 bbe50343dd16
child 4773 2cc2e0e0c6ec
update 3.6.13 branch with rl_safe_eval & rl_config.toColorCanUse changes
.hgignore
CHANGES.md
src/reportlab/lib/colors.py
src/reportlab/lib/rl_safe_eval.py
src/reportlab/lib/utils.py
src/reportlab/rl_settings.py
tests/test_lib_rl_safe_eval.py
--- a/.hgignore	Mon Nov 21 10:58:04 2022 +0000
+++ b/.hgignore	Mon Apr 24 13:52:40 2023 +0100
@@ -63,7 +63,7 @@
 ^tests/test_pdfgen_pycanvas_out\.txt
 ^tests/test_render(SVG|PS)_output\.html
 ^tests/test_source_chars\.txt
-^tests/test_graphics_images(|-cairo)\.(png|gif)
+^tests/test_graphics_images(|-(cairo|libart))\.(png|gif)
 ^tests/barcode-out
 ^tests/render-out
 ^tests/charts-out
@@ -76,3 +76,11 @@
 ^src/reportlab.egg-info/
 ^.git
 ^.gitignore
+^[lL]ib(64|)/
+^[lL]ib64
+^[iI]nclude/
+^share/
+^local/
+^(bin|[Ss]cripts)/
+^pyvenv.cfg
+^reportlab_(settings|mod)\.py
--- a/CHANGES.md	Mon Nov 21 10:58:04 2022 +0000
+++ b/CHANGES.md	Mon Apr 24 13:52:40 2023 +0100
@@ -11,6 +11,10 @@
 The contributors lists are in no order and apologies to those accidentally not
 mentioned. If we missed you, please let us know!
 
+CHANGES  3.6.14  24/04/2023
+---------------------------
+	* implement a safer toColor with rl_config.toColorCanUse option and rl_extended_literal_eval
+
 CHANGES  3.6.13  21/11/2022
 ---------------------------
 	* fixes for python 3.12.0a1
--- a/src/reportlab/lib/colors.py	Mon Nov 21 10:58:04 2022 +0000
+++ b/src/reportlab/lib/colors.py	Mon Apr 24 13:52:40 2023 +0100
@@ -41,7 +41,8 @@
 '''
 import math, re, functools
 from reportlab.lib.rl_accel import fp_str
-from reportlab.lib.utils import asNative, isStr, rl_safe_eval
+from reportlab.lib.utils import asNative, isStr, rl_safe_eval, rl_extended_literal_eval
+from reportlab import rl_config
 from ast import literal_eval
 
 class Color:
@@ -835,6 +836,17 @@
 cssParse=cssParse()
 
 class toColor:
+    """Accepot an expression returnng a Color subclass.
+
+    This used to accept arbitrary Python expressions, which resulted in increasngly devilish CVEs and
+    security holes from tie to time.  In April 2023 we are creating explicit, "dumb" parsing code to
+    replace this.  Acceptable patterns are
+
+    a Color instance passed in by the Python programmer
+    a named list of colours ('pink' etc')
+    list of 3 or 4 numbers
+    all CSS colour expression
+    """
     _G = {} #globals we like (eventually)
 
     def __init__(self):
@@ -860,20 +872,57 @@
             C = getAllNamedColors()
             s = arg.lower()
             if s in C: return C[s]
-            G = C.copy()
-            G.update(self.extraColorsNS)
-            if not self._G:
+
+
+            # allow expressions like 'Blacker(red, 0.5)'
+            # >>> re.compile(r"(Blacker|Whiter)\((\w+)\,\s?([0-9.]+)\)").match(msg).groups()
+            # ('Blacker', 'red', '0.5')
+            # >>> 
+            pat = re.compile(r"(Blacker|Whiter)\((\w+)\,\s?([0-9.]+)\)")
+            m = pat.match(arg)
+            if m:
+                funcname, rootcolor, num = m.groups()
+                if funcname == 'Blacker':
+                    return Blacker(rootcolor, float(num))
+                else:
+                    return Whiter(rootcolor, float(num))
+
+            try:
+                import ast
+                expr = ast.literal_eval(arg)    #safe probably only a tuple or list of values
+                return toColor(expr)
+            except (SyntaxError, ValueError):
+                pass
+
+            if rl_config.toColorCanUse=='rl_safe_eval':
+                #the most dangerous option
+                G = C.copy()
+                G.update(self.extraColorsNS)
+                if not self._G:
+                    C = globals()
+                    self._G = {s:C[s] for s in '''Blacker CMYKColor CMYKColorSep Color ColorType HexColor PCMYKColor PCMYKColorSep Whiter
+                        _chooseEnforceColorSpace _enforceCMYK _enforceError _enforceRGB _enforceSEP _enforceSEP_BLACK
+                        _enforceSEP_CMYK _namedColors _re_css asNative cmyk2rgb cmykDistance color2bw colorDistance
+                        cssParse describe fade fp_str getAllNamedColors hsl2rgb hue2rgb isStr linearlyInterpolatedColor
+                        literal_eval obj_R_G_B opaqueColor rgb2cmyk setColors toColor toColorOrNone'''.split()}
+                G.update(self._G)
+                try:
+                    return toColor(rl_safe_eval(arg,g=G,l={}))
+                except:
+                    pass
+            elif rl_config.toColorCanUse=='rl_extended_literal_eval':
                 C = globals()
-                self._G = {s:C[s] for s in '''Blacker CMYKColor CMYKColorSep Color ColorType HexColor PCMYKColor PCMYKColorSep Whiter
-                    _chooseEnforceColorSpace _enforceCMYK _enforceError _enforceRGB _enforceSEP _enforceSEP_BLACK
-                    _enforceSEP_CMYK _namedColors _re_css asNative cmyk2rgb cmykDistance color2bw colorDistance
-                    cssParse describe fade fp_str getAllNamedColors hsl2rgb hue2rgb isStr linearlyInterpolatedColor
-                    literal_eval obj_R_G_B opaqueColor rgb2cmyk setColors toColor toColorOrNone'''.split()}
-            G.update(self._G)
-            try:
-                return toColor(rl_safe_eval(arg,g=G,l={}))
-            except:
-                pass
+                S = getAllNamedColors().copy()
+                C = {k:C[k] for k in '''Blacker CMYKColor CMYKColorSep Color ColorType HexColor PCMYKColor PCMYKColorSep Whiter
+                        _chooseEnforceColorSpace _enforceCMYK _enforceError _enforceRGB _enforceSEP _enforceSEP_BLACK
+                        _enforceSEP_CMYK _namedColors _re_css asNative cmyk2rgb cmykDistance color2bw colorDistance
+                        cssParse describe fade fp_str getAllNamedColors hsl2rgb hue2rgb linearlyInterpolatedColor
+                        obj_R_G_B opaqueColor rgb2cmyk setColors toColor toColorOrNone'''.split()
+                        if callable(C.get(k,None))}
+                try:
+                    return rl_extended_literal_eval(arg,C,S)
+                except (ValueError, SyntaxError):
+                    pass
 
         try:
             return HexColor(arg)
--- a/src/reportlab/lib/rl_safe_eval.py	Mon Nov 21 10:58:04 2022 +0000
+++ b/src/reportlab/lib/rl_safe_eval.py	Mon Apr 24 13:52:40 2023 +0100
@@ -3,7 +3,7 @@
 #https://github.com/zopefoundation/RestrictedPython
 #https://github.com/danthedeckie/simpleeval
 #hopefully we are standing on giants' shoulders
-import sys, os, ast, re, weakref, time, copy, math
+import sys, os, ast, re, weakref, time, copy, math, types
 eval_debug = int(os.environ.get('EVAL_DEBUG','0'))
 strTypes = (bytes,str)
 isPy39 = sys.version_info[:2]>=(3,9)
@@ -53,7 +53,9 @@
 						func_doc func_globals func_name gi_code gi_frame gi_running gi_yieldfrom
 						__globals__ im_class im_func im_self __iter__ __kwdefaults__ __module__
 						__name__ next __qualname__ __self__ tb_frame tb_lasti tb_lineno tb_next
-						globals vars locals'''.split()
+						globals vars locals
+						type eval exec aiter anext compile open
+						dir print classmethod staticmethod __import__ super property'''.split()
 						)
 __rl_unsafe_re__ = re.compile(r'\b(?:%s)' % '|'.join(__rl_unsafe__),re.M)
 
@@ -1204,5 +1206,70 @@
 class __rl_safe_exec__(__rl_safe_eval__):
 	mode = 'exec'
 
+def rl_extended_literal_eval(expr, safe_callables=None, safe_names=None):
+	if safe_callables is None:
+		safe_callables = {}
+	if safe_names is None:
+		safe_names = {}
+	safe_names = safe_names.copy()
+	safe_names.update({'None': None, 'True': True, 'False': False})
+	#make these readonly with MappingProxyType
+	safe_names = types.MappingProxyType(safe_names)
+	safe_callables = types.MappingProxyType(safe_callables)
+	if isinstance(expr, str):
+		expr = ast.parse(expr, mode='eval')
+	if isinstance(expr, ast.Expression):
+		expr = expr.body
+	try:
+		# Python 3.4 and up
+		ast.NameConstant
+		safe_test = lambda n: isinstance(n, ast.NameConstant) or isinstance(n,ast.Name) and n.id in safe_names
+		safe_extract = lambda n: n.value if isinstance(n,ast.NameConstant) else safe_names[n.id]
+	except AttributeError:
+		# Everything before
+		safe_test = lambda n: isinstance(n, ast.Name) and n.id in safe_names
+		safe_extract = lambda n: safe_names[n.id]
+	def _convert(node):
+		if isinstance(node, (ast.Str, ast.Bytes)):
+			return node.s
+		elif isinstance(node, ast.Num):
+			return node.n
+		elif isinstance(node, ast.Tuple):
+			return tuple(map(_convert, node.elts))
+		elif isinstance(node, ast.List):
+			return list(map(_convert, node.elts))
+		elif isinstance(node, ast.Dict):
+			return dict((_convert(k), _convert(v)) for k, v
+						in zip(node.keys, node.values))
+		elif safe_test(node):
+			return safe_extract(node)
+		elif isinstance(node, ast.UnaryOp) and \
+			 isinstance(node.op, (ast.UAdd, ast.USub)) and \
+			 isinstance(node.operand, (ast.Num, ast.UnaryOp, ast.BinOp)):
+			operand = _convert(node.operand)
+			if isinstance(node.op, ast.UAdd):
+				return + operand
+			else:
+				return - operand
+		elif isinstance(node, ast.BinOp) and \
+			 isinstance(node.op, (ast.Add, ast.Sub)) and \
+			 isinstance(node.right, (ast.Num, ast.UnaryOp, ast.BinOp)) and \
+			 isinstance(node.right.n, complex) and \
+			 isinstance(node.left, (ast.Num, ast.UnaryOp, astBinOp)):
+			left = _convert(node.left)
+			right = _convert(node.right)
+			if isinstance(node.op, ast.Add):
+				return left + right
+			else:
+				return left - right
+		elif isinstance(node, ast.Call) and \
+			 isinstance(node.func, ast.Name) and \
+			 node.func.id in safe_callables:
+			return safe_callables[node.func.id](
+				*[_convert(n) for n in node.args],
+				**{kw.arg: _convert(kw.value) for kw in node.keywords})
+		raise ValueError('Bad expression')
+	return _convert(expr)
+
 rl_safe_exec = __rl_safe_exec__()
 rl_safe_eval = __rl_safe_eval__()
--- a/src/reportlab/lib/utils.py	Mon Nov 21 10:58:04 2022 +0000
+++ b/src/reportlab/lib/utils.py	Mon Apr 24 13:52:40 2023 +0100
@@ -11,7 +11,7 @@
 from hashlib import md5
 
 from reportlab.lib.rltempfile import get_rl_tempfile, get_rl_tempdir
-from . rl_safe_eval import rl_safe_exec, rl_safe_eval, safer_globals
+from . rl_safe_eval import rl_safe_exec, rl_safe_eval, safer_globals, rl_extended_literal_eval
 from PIL import Image
 
 class __UNSET__:
--- a/src/reportlab/rl_settings.py	Mon Nov 21 10:58:04 2022 +0000
+++ b/src/reportlab/rl_settings.py	Mon Apr 24 13:52:40 2023 +0100
@@ -69,7 +69,8 @@
 trustedSchemes
 renderPMBackend
 xmlParser
-textPaths'''.split())
+textPaths
+toColorCanUse'''.split())
 
 allowTableBoundsErrors =    1 # set to 0 to die on too large elements in tables in debug (recommend 1 for production use)
 shapeChecking =             1
@@ -163,6 +164,7 @@
 textPaths='backend'                                 #freetype or _renderPM or backend
                                                     #determines what code is used to create Paths from str
                                                     #see reportlab/graphics/utils.py for full horror
+toColorCanUse='rl_extended_literal_eval'            #change to None or 'rl_safe_eval' depending on trust
 
 # places to look for T1Font information
 T1SearchPath =  (
--- a/tests/test_lib_rl_safe_eval.py	Mon Nov 21 10:58:04 2022 +0000
+++ b/tests/test_lib_rl_safe_eval.py	Mon Apr 24 13:52:40 2023 +0100
@@ -1,6 +1,6 @@
 #Copyright ReportLab Europe Ltd. 2000-2017
 #see license.txt for license details
-"""Tests for reportlab.lib.rl_eval
+"""Tests for reportlab.lib.rl_safe_eval
 """
 __version__='3.5.33'
 from reportlab.lib.testutils import setOutDir,makeSuiteForClasses, printLocation
@@ -10,7 +10,7 @@
 from reportlab import rl_config
 import unittest
 from reportlab.lib import colors
-from reportlab.lib.utils import rl_safe_eval, rl_safe_exec, annotateException
+from reportlab.lib.utils import rl_safe_eval, rl_safe_exec, annotateException, rl_extended_literal_eval
 from reportlab.lib.rl_safe_eval import BadCode
 
 testObj = [1,('a','b',2),{'A':1,'B':2.0},"32"]
@@ -52,7 +52,6 @@
                 'dict(a=1).get("a",2)',
                 'dict(a=1).pop("a",2)',
                 '{"_":1+_ for _ in (1,2)}.pop(1,None)',
-                '(type(1),type(str),type(testObj),type(TestClass))',
                 '1 if True else "a"',
                 '1 if False else "a"',
                 'testFunc(bad=False)',
@@ -74,6 +73,8 @@
                 (
                 'fail',
                 (
+                'vars()',
+                '(type(1),type(str),type(testObj),type(TestClass))',
                 'open("/tmp/myfile")',
                 'SafeEvalTestCase.__module__',
                 ("testInst.__class__.__bases__[0].__subclasses__()",dict(g=dict(testInst=testInst))),
@@ -97,6 +98,8 @@
                 'testFunc(bad=True)',
                 'getattr(testInst,"__class__",14)',
                 '"{1}{2}".format(1,2)',
+                'builtins',
+                '[ [ [ [ ftype(ctype(0, 0, 0, 0, 3, 67, b"t\\x00d\\x01\\x83\\x01\\xa0\\x01d\\x02\\xa1\\x01\\x01\\x00d\\x00S\\x00", (None, "os", "touch /tmp/exploited"), ("__import__", "system"), (), "<stdin>", "", 1, b"\\x12\\x01"), {})() for ftype in [type(lambda: None)] ] for ctype in [type(getattr(lambda: {None}, Word("__code__")))] ] for Word in [orgTypeFun("Word", (str,), { "mutated": 1, "startswith": lambda self, x: False, "__eq__": lambda self,x: self.mutate() and self.mutated < 0 and str(self) == x, "mutate": lambda self: {setattr(self, "mutated", self.mutated - 1)}, "__hash__": lambda self: hash(str(self)) })] ] for orgTypeFun in [type(type(1))]] and "red"',
                 )
                 ),
                 ):
@@ -155,8 +158,46 @@
     def test_002(self):
         self.assertTrue(rl_safe_eval("GA=='ga'"))
 
+class ExtendedLiteralEval(unittest.TestCase):
+    def test_001(self):
+        S = colors.getAllNamedColors().copy()
+        C = {s:getattr(colors,s) for s in '''Blacker CMYKColor CMYKColorSep Color ColorType HexColor PCMYKColor PCMYKColorSep Whiter
+                        _chooseEnforceColorSpace _enforceCMYK _enforceError _enforceRGB _enforceSEP _enforceSEP_BLACK
+                        _enforceSEP_CMYK _namedColors _re_css asNative cmyk2rgb cmykDistance color2bw colorDistance
+                        cssParse describe fade fp_str getAllNamedColors hsl2rgb hue2rgb linearlyInterpolatedColor
+                        obj_R_G_B opaqueColor rgb2cmyk setColors toColor toColorOrNone'''.split()
+                        if callable(getattr(colors,s,None))}
+        def showVal(s):
+            try:
+                r = rl_extended_literal_eval(s,C,S)
+            except:
+                r = str(sys.exc_info()[1])
+            return r
+
+        for expr, expected in (
+                ('1.0', 1.0),
+                ('1', 1),
+                ('red', colors.red),
+                ('True', True),
+                ('False', False),
+                ('None', None),
+                ('Blacker(red,0.5)', colors.Color(.5,0,0,1)),
+                ('PCMYKColor(21,10,30,5,spotName="ABCD")', colors.PCMYKColor(21,10,30,5,spotName='ABCD',alpha=100)),
+                ('HexColor("#ffffff")', colors.Color(1,1,1,1)),
+                ('linearlyInterpolatedColor(red, blue, 0, 1, 0.5)', colors.Color(.5,0,.5,1)),
+                ('red.rgb()', 'Bad expression'),
+                ('__import__("sys")', 'Bad expression'),
+                ('globals()', 'Bad expression'),
+                ('locals()', 'Bad expression'),
+                ('vars()', 'Bad expression'),
+                ('builtins', 'Bad expression'),
+                ('__file__', 'Bad expression'),
+                ('__name__', 'Bad expression'),
+                ):
+            self.assertEqual(showVal(expr),expected,f"rl_extended_literal_eval({expr!r}) is not equal to expected {expected}")
+
 def makeSuite():
-    return makeSuiteForClasses(SafeEvalTestCase,SafeEvalTestBasics)
+    return makeSuiteForClasses(SafeEvalTestCase,SafeEvalTestBasics,ExtendedLiteralEval)
 
 if __name__ == "__main__": #noruntests
     unittest.TextTestRunner().run(makeSuite())