Skip to content

Commit

Permalink
added rl_extended_literal_eval used in toColor with control by rl_con…
Browse files Browse the repository at this point in the history
…fig.toColorCanUse
  • Loading branch information
robin committed Apr 24, 2023
1 parent a32f51c commit 02fa2dd
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 62 deletions.
4 changes: 3 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ mentioned. If we missed you, please let us know!

CHANGES 4.0.0a3 18/04/2023
---------------------------
* Allow ListFlowable to have a caption
* initial support for rml ul ol dl tagging
* added support for an ol/ul/dl caption paragraph
* implement a safer toColor with rl_config.toColorCanUse option and rl_extended_literal_eval

CHANGES 4.0.0a2 14/03/2023
---------------------------
Expand Down
85 changes: 32 additions & 53 deletions src/reportlab/lib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -890,62 +891,40 @@ def __call__(self,arg,default=None):

try:
import ast
expr = ast.literal_eval(arg)
expr = ast.literal_eval(arg) #safe probably only a tuple or list of values
return toColor(expr)

except (SyntaxError, ValueError):
pass


##################################################################################

allowedColorClasses = '''Color CMYKColor PCMYKColor CMYKColorSep PCMYKColorSep'''
def get_class_instance(class_string):
"Explicit parser for simple class instantiations. We do NOT allow arithmetic in attributes"
pattern = r'^(\w+)\((.*)\)$'
m = re.match(pattern, class_string)
if m:
class_name = m.group(1)
args_str = m.group(2)
args = []
kwargs = {}
for arg in args_str.split(","):
if '=' in arg:
key, value = arg.split('=')[0:2]
kwargs[key] = value
else:
try:
arg = float(arg)
args.append(arg)
except ValueError:
args.append(arg)

if class_name in allowedColorClasses:
class_obj = globals().get(class_name)
instance = class_obj(*args, **kwargs)
return instance
raise ValueError('Invalid color object %r' % (class_name))
###################################################################################
inst = get_class_instance(arg)
if inst is not None:
return inst

raise ValueError('Invalid color value %r' % arg)

# end of string path

# if True: #*TODO* replace with rl_config 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)

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)
Expand Down
69 changes: 67 additions & 2 deletions src/reportlab/lib/rl_safe_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -54,7 +54,7 @@ class BadCode(ValueError):
__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
type eval exec aiter anext classmethod compile dir open
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)
Expand Down Expand Up @@ -1206,5 +1206,70 @@ def __call__(self, expr, g=None, l=None, timeout=None, allowed_magic_methods=Non
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__()
2 changes: 1 addition & 1 deletion src/reportlab/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__:
Expand Down
4 changes: 3 additions & 1 deletion src/reportlab/rl_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -163,6 +164,7 @@
textPaths='freetype' #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 = (
Expand Down
6 changes: 5 additions & 1 deletion tests/test_lib_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ def test3(self):

# Make a roundtrip test (RGB > CMYK > RGB).
for name, rgbCol in rgbCols:
r1, g1, b1 = rgbCol.red, rgbCol.green, rgbCol.blue
try:
r1, g1, b1 = rgbCol.red, rgbCol.green, rgbCol.blue
except:
print(name,rgbCol)
raise
c, m, y, k = colors.rgb2cmyk(r1, g1, b1)
r2, g2, b2 = colors.cmyk2rgb((c, m, y, k))
rgbCol2 = colors.Color(r2, g2, b2)
Expand Down
47 changes: 44 additions & 3 deletions tests/test_lib_rl_safe_eval.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"]
Expand Down Expand Up @@ -73,6 +73,7 @@ def test(self):
(
'fail',
(
'vars()',
'(type(1),type(str),type(testObj),type(TestClass))',
'open("/tmp/myfile")',
'SafeEvalTestCase.__module__',
Expand All @@ -97,6 +98,8 @@ def test(self):
'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"',
)
),
):
Expand Down Expand Up @@ -155,8 +158,46 @@ def test_001(self):
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())
Expand Down

0 comments on commit 02fa2dd

Please sign in to comment.