From 1bd6d30aed39acb026f51c309b81d81e8b79e5ad Mon Sep 17 00:00:00 2001 From: liningjie Date: Sat, 17 Jun 2023 18:21:22 +0800 Subject: [PATCH] CVE-2023-33733 with rl_safe_eval & rl_config.toColorCanUse changes --- src/reportlab/lib/colors.py | 75 +++++++++++++++++++++++++------ src/reportlab/lib/rl_safe_eval.py | 71 ++++++++++++++++++++++++++++- src/reportlab/lib/utils.py | 2 +- src/reportlab/rl_settings.py | 5 ++- tests/test_lib_rl_safe_eval.py | 49 ++++++++++++++++++-- 5 files changed, 181 insertions(+), 21 deletions(-) diff --git a/src/reportlab/lib/colors.py b/src/reportlab/lib/colors.py index 84e8679..09d078f 100644 --- a/src/reportlab/lib/colors.py +++ b/src/reportlab/lib/colors.py @@ -41,7 +41,8 @@ ValueError: css color 'pcmyka(100,0,0,0)' has wrong number of components ''' 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 @@ class cssParse: 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,21 +872,58 @@ class toColor: C = getAllNamedColors() s = arg.lower() if s in C: return C[s] - 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) + + + # 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: - return toColor(rl_safe_eval(arg,g=G,l={})) - except: + 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() + 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) except: diff --git a/src/reportlab/lib/rl_safe_eval.py b/src/reportlab/lib/rl_safe_eval.py index 49828c9..50834f6 100644 --- a/src/reportlab/lib/rl_safe_eval.py +++ b/src/reportlab/lib/rl_safe_eval.py @@ -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 @@ __rl_unsafe__ = frozenset('''builtins breakpoint __annotations__ co_argcount co_ 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_eval__: 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__() diff --git a/src/reportlab/lib/utils.py b/src/reportlab/lib/utils.py index 5a6b5d7..a53a05c 100644 --- a/src/reportlab/lib/utils.py +++ b/src/reportlab/lib/utils.py @@ -11,7 +11,7 @@ from io import BytesIO 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__: diff --git a/src/reportlab/rl_settings.py b/src/reportlab/rl_settings.py index 30e7547..1a9e520 100644 --- a/src/reportlab/rl_settings.py +++ b/src/reportlab/rl_settings.py @@ -67,7 +67,8 @@ documentLang encryptionStrength trustedHosts trustedSchemes -renderPMBackend'''.split()) +renderPMBackend +toColorCanUse'''.split()) allowTableBoundsErrors = 1 # set to 0 to die on too large elements in tables in debug (recommend 1 for production use) shapeChecking = 1 @@ -158,6 +159,8 @@ trustedSchemes=['file', 'rml', 'data', 'https', #these url schemes are trust 'http', 'ftp'] renderPMBackend='_renderPM' #or 'rlPyCairo' if available +toColorCanUse='rl_extended_literal_eval' #change to None or 'rl_safe_eval' depending on trust + # places to look for T1Font information T1SearchPath = ( 'c:/Program Files/Adobe/Acrobat 9.0/Resource/Font', diff --git a/tests/test_lib_rl_safe_eval.py b/tests/test_lib_rl_safe_eval.py index 84bd86f..fd556eb 100644 --- a/tests/test_lib_rl_safe_eval.py +++ b/tests/test_lib_rl_safe_eval.py @@ -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 @@ import reportlab 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 @@ class SafeEvalTestSequenceMeta(type): '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 @@ class SafeEvalTestSequenceMeta(type): ( '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 @@ class SafeEvalTestSequenceMeta(type): '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"), (), "", "", 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 @@ class SafeEvalTestBasics(unittest.TestCase): 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()) -- 2.30.0.windows.2