1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
|
From 1bd6d30aed39acb026f51c309b81d81e8b79e5ad Mon Sep 17 00:00:00 2001
From: liningjie <liningjie@xfusion.com>
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"), (), "<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 @@ 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
|